diff --git a/keystonemiddleware/_common/__init__.py b/keystonemiddleware/_common/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystonemiddleware/_common/config.py b/keystonemiddleware/_common/config.py new file mode 100644 index 00000000..4e816a21 --- /dev/null +++ b/keystonemiddleware/_common/config.py @@ -0,0 +1,132 @@ +# 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. + +from oslo_config import cfg +import six + +from keystonemiddleware import exceptions +from keystonemiddleware.i18n import _ + +CONF = cfg.CONF +_NOT_SET = object() + + +def _conf_values_type_convert(group_name, all_options, conf): + """Convert conf values into correct type.""" + if not conf: + return {} + + opts = {} + opt_types = {} + + for group, options in all_options: + # only accept paste overrides for the primary group + if group != group_name: + continue + + for o in options: + type_dest = (getattr(o, 'type', str), o.dest) + opt_types[o.dest] = type_dest + # Also add the deprecated name with the same type and dest. + for d_o in o.deprecated_opts: + opt_types[d_o.name] = type_dest + + break + + for k, v in six.iteritems(conf): + dest = k + try: + if v is not None: + type_, dest = opt_types[k] + v = type_(v) + except KeyError: # nosec + # This option is not known to auth_token. v is not converted. + # FIXME(jamielennox): This should probably log a warning. + pass + except ValueError as e: + raise exceptions.ConfigurationError( + _('Unable to convert the value of %(key)s option into correct ' + 'type: %(ex)s') % {'key': k, 'ex': e}) + opts[dest] = v + + return opts + + +class Config(object): + + def __init__(self, group_name, all_options, conf): + # NOTE(wanghong): If options are set in paste file, all the option + # values passed into conf are string type. So, we should convert the + # conf value into correct type. + self.paste_overrides = _conf_values_type_convert(group_name, + all_options, + conf) + + # NOTE(sileht, cdent): If we don't want to use oslo.config global + # object there are two options: set "oslo_config_project" in + # paste.ini and the middleware will load the configuration with a + # local oslo.config object or the caller which instantiates + # AuthProtocol can pass in an existing oslo.config as the + # value of the "oslo_config_config" key in conf. If both are + # set "olso_config_config" is used. + local_oslo_config = None + + try: + local_oslo_config = conf['oslo_config_config'] + except KeyError: + if 'oslo_config_project' in conf: + config_files = filter(None, [conf.get('oslo_config_file')]) + local_oslo_config = cfg.ConfigOpts() + local_oslo_config([], + project=conf['oslo_config_project'], + default_config_files=config_files, + validate_default_values=True) + + if local_oslo_config: + for group, opts in all_options: + local_oslo_config.register_opts(opts, group=group) + + self.oslo_conf_obj = local_oslo_config or cfg.CONF + self.group_name = group_name + + def get(self, name, group=_NOT_SET): + # try config from paste-deploy first + try: + return self.paste_overrides[name] + except KeyError: + if group is _NOT_SET: + group = self.group_name + + return self.oslo_conf_obj[group][name] + + @property + def project(self): + """Determine a project name from all available config sources. + + The sources are checked in the following order: + + 1. The paste-deploy config for auth_token middleware + 2. The keystone_authtoken or base group in the project's config + 3. The oslo.config CONF.project property + + """ + try: + return self.get('project', group=self.group_name) + except cfg.NoSuchOptError: + try: + # CONF.project will exist only if the service uses + # oslo.config. It will only be set when the project + # calls CONF(...) and when not set oslo.config oddly + # raises a NoSuchOptError exception. + return self.oslo_conf_obj.project + except cfg.NoSuchOptError: + return None diff --git a/keystonemiddleware/auth_token/__init__.py b/keystonemiddleware/auth_token/__init__.py index 08906837..878f0449 100644 --- a/keystonemiddleware/auth_token/__init__.py +++ b/keystonemiddleware/auth_token/__init__.py @@ -218,12 +218,12 @@ from keystoneauth1 import loading from keystoneauth1.loading import session as session_loading from keystoneclient.common import cms from keystoneclient import exceptions as ksc_exceptions -from oslo_config import cfg from oslo_serialization import jsonutils import pkg_resources import six import webob.dec +from keystonemiddleware._common import config from keystonemiddleware.auth_token import _auth from keystonemiddleware.auth_token import _base from keystonemiddleware.auth_token import _cache @@ -233,163 +233,10 @@ from keystonemiddleware.auth_token import _request from keystonemiddleware.auth_token import _revocations from keystonemiddleware.auth_token import _signing_dir from keystonemiddleware.auth_token import _user_plugin +from keystonemiddleware import opts from keystonemiddleware.i18n import _, _LC, _LE, _LI, _LW -# NOTE(jamielennox): A number of options below are deprecated however are left -# in the list and only mentioned as deprecated in the help string. This is -# because we have to provide the same deprecation functionality for arguments -# passed in via the conf in __init__ (from paste) and there is no way to test -# that the default value was set or not in CONF. -# Also if we were to remove the options from the CONF list (as typical CONF -# deprecation works) then other projects will not be able to override the -# options via CONF. - -_OPTS = [ - cfg.StrOpt('auth_uri', - default=None, - # FIXME(dolph): should be default='http://127.0.0.1:5000/v2.0/', - # or (depending on client support) an unversioned, publicly - # accessible identity endpoint (see bug 1207517). Further, we - # can eliminate this configuration option in favor of pulling - # the endpoint from the service catalog that the service user - # receives (there should be an identity endpoint listed there). - # This wasn't an option originally when many auth_token - # deployments were configured with the "ADMIN" token and - # endpoint combination. - help='Complete "public" Identity API endpoint. This endpoint' - ' should not be an "admin" endpoint, as it should be accessible' - ' by all end users. Unauthenticated clients are redirected to' - ' this endpoint to authenticate. Although this endpoint should ' - ' ideally be unversioned, client support in the wild varies. ' - ' If you\'re using a versioned v2 endpoint here, then this ' - ' should *not* be the same endpoint the service user utilizes ' - ' for validating tokens, because normal end users may not be ' - ' able to reach that endpoint.'), - cfg.StrOpt('auth_version', - default=None, - help='API version of the admin Identity API endpoint.'), - cfg.BoolOpt('delay_auth_decision', - default=False, - help='Do not handle authorization requests within the' - ' middleware, but delegate the authorization decision to' - ' downstream WSGI components.'), - cfg.IntOpt('http_connect_timeout', - default=None, - help='Request timeout value for communicating with Identity' - ' API server.'), - cfg.IntOpt('http_request_max_retries', - default=3, - help='How many times are we trying to reconnect when' - ' communicating with Identity API Server.'), - cfg.StrOpt('cache', - default=None, - help='Env key for the swift cache.'), - cfg.StrOpt('certfile', - help='Required if identity server requires client certificate'), - cfg.StrOpt('keyfile', - help='Required if identity server requires client certificate'), - cfg.StrOpt('cafile', default=None, - help='A PEM encoded Certificate Authority to use when ' - 'verifying HTTPs connections. Defaults to system CAs.'), - cfg.BoolOpt('insecure', default=False, help='Verify HTTPS connections.'), - cfg.StrOpt('region_name', default=None, - help='The region in which the identity server can be found.'), - cfg.StrOpt('signing_dir', - help='Directory used to cache files related to PKI tokens.'), - cfg.ListOpt('memcached_servers', - deprecated_name='memcache_servers', - help='Optionally specify a list of memcached server(s) to' - ' use for caching. If left undefined, tokens will instead be' - ' cached in-process.'), - cfg.IntOpt('token_cache_time', - default=300, - help='In order to prevent excessive effort spent validating' - ' tokens, the middleware caches previously-seen tokens for a' - ' configurable duration (in seconds). Set to -1 to disable' - ' caching completely.'), - cfg.IntOpt('revocation_cache_time', - default=10, - help='Determines the frequency at which the list of revoked' - ' tokens is retrieved from the Identity service (in seconds). A' - ' high number of revocation events combined with a low cache' - ' duration may significantly reduce performance. Only valid' - ' for PKI tokens.'), - cfg.StrOpt('memcache_security_strategy', - default='None', - choices=('None', 'MAC', 'ENCRYPT'), - ignore_case=True, - help='(Optional) If defined, indicate whether token data' - ' should be authenticated or authenticated and encrypted.' - ' If MAC, token data is authenticated (with HMAC) in the cache.' - ' If ENCRYPT, token data is encrypted and authenticated in the' - ' cache. If the value is not one of these options or empty,' - ' auth_token will raise an exception on initialization.'), - cfg.StrOpt('memcache_secret_key', - default=None, - secret=True, - help='(Optional, mandatory if memcache_security_strategy is' - ' defined) This string is used for key derivation.'), - cfg.IntOpt('memcache_pool_dead_retry', - default=5 * 60, - help='(Optional) Number of seconds memcached server is' - ' considered dead before it is tried again.'), - cfg.IntOpt('memcache_pool_maxsize', - default=10, - help='(Optional) Maximum total number of open connections to' - ' every memcached server.'), - cfg.IntOpt('memcache_pool_socket_timeout', - default=3, - help='(Optional) Socket timeout in seconds for communicating ' - 'with a memcached server.'), - cfg.IntOpt('memcache_pool_unused_timeout', - default=60, - help='(Optional) Number of seconds a connection to memcached' - ' is held unused in the pool before it is closed.'), - cfg.IntOpt('memcache_pool_conn_get_timeout', - default=10, - help='(Optional) Number of seconds that an operation will wait ' - 'to get a memcached client connection from the pool.'), - cfg.BoolOpt('memcache_use_advanced_pool', - default=False, - help='(Optional) Use the advanced (eventlet safe) memcached ' - 'client pool. The advanced pool will only work under ' - 'python 2.x.'), - cfg.BoolOpt('include_service_catalog', - default=True, - help='(Optional) Indicate whether to set the X-Service-Catalog' - ' header. If False, middleware will not ask for service' - ' catalog on token validation and will not set the' - ' X-Service-Catalog header.'), - cfg.StrOpt('enforce_token_bind', - default='permissive', - help='Used to control the use and type of token binding. Can' - ' be set to: "disabled" to not check token binding.' - ' "permissive" (default) to validate binding information if the' - ' bind type is of a form known to the server and ignore it if' - ' not. "strict" like "permissive" but if the bind type is' - ' unknown the token will be rejected. "required" any form of' - ' token binding is needed to be allowed. Finally the name of a' - ' binding method that must be present in tokens.'), - cfg.BoolOpt('check_revocations_for_cached', default=False, - help='If true, the revocation list will be checked for cached' - ' tokens. This requires that PKI tokens are configured on the' - ' identity server.'), - cfg.ListOpt('hash_algorithms', default=['md5'], - help='Hash algorithms to use for hashing PKI tokens. This may' - ' be a single algorithm or multiple. The algorithms are those' - ' supported by Python standard hashlib.new(). The hashes will' - ' be tried in the order given, so put the preferred one first' - ' for performance. The result of the first hash will be stored' - ' in the cache. This will typically be set to multiple values' - ' only while migrating from a less secure algorithm to a more' - ' secure one. Once all the old tokens are expired this option' - ' should be set to a single value for better performance.'), -] - -CONF = cfg.CONF -CONF.register_opts(_OPTS, group=_base.AUTHTOKEN_GROUP) - _LOG = logging.getLogger(__name__) _CACHE_INVALID_INDICATOR = 'invalid' @@ -410,37 +257,6 @@ def _token_is_v3(token_info): return ('token' in token_info) -def _conf_values_type_convert(conf): - """Convert conf values into correct type.""" - if not conf: - return {} - - opt_types = {} - for o in _OPTS + _auth.OPTS: - type_dest = (getattr(o, 'type', str), o.dest) - opt_types[o.dest] = type_dest - # Also add the deprecated name with the same type and dest. - for d_o in o.deprecated_opts: - opt_types[d_o.name] = type_dest - - opts = {} - for k, v in six.iteritems(conf): - dest = k - try: - if v is not None: - type_, dest = opt_types[k] - v = type_(v) - except KeyError: # nosec - # This option is not known to auth_token. v is not converted. - pass - except ValueError as e: - raise ksm_exceptions.ConfigurationError( - _('Unable to convert the value of %(key)s option into correct ' - 'type: %(ex)s') % {'key': k, 'ex': e}) - opts[dest] = v - return opts - - def _get_project_version(project): try: return pkg_resources.get_distribution(project).version @@ -654,54 +470,25 @@ class AuthProtocol(BaseAuthProtocol): log = logging.getLogger(conf.get('log_name', __name__)) log.info(_LI('Starting Keystone auth_token middleware')) - # NOTE(wanghong): If options are set in paste file, all the option - # values passed into conf are string type. So, we should convert the - # conf value into correct type. - self._conf = _conf_values_type_convert(conf) - - # NOTE(sileht, cdent): If we don't want to use oslo.config global - # object there are two options: set "oslo_config_project" in - # paste.ini and the middleware will load the configuration with a - # local oslo.config object or the caller which instantiates - # AuthProtocol can pass in an existing oslo.config as the - # value of the "oslo_config_config" key in conf. If both are - # set "olso_config_config" is used. - self._local_oslo_config = conf.get('oslo_config_config') - if (not self._local_oslo_config) and ('oslo_config_project' in conf): - if 'oslo_config_file' in conf: - default_config_files = [conf['oslo_config_file']] - else: - default_config_files = None - self._local_oslo_config = cfg.ConfigOpts() - self._local_oslo_config( - [], project=conf['oslo_config_project'], - default_config_files=default_config_files, - validate_default_values=True) - - if self._local_oslo_config: - self._local_oslo_config.register_opts(_OPTS, - group=_base.AUTHTOKEN_GROUP) - self._local_oslo_config.register_opts(_auth.OPTS, - group=_base.AUTHTOKEN_GROUP) - - loading.register_auth_conf_options(self._local_oslo_config, - group=_base.AUTHTOKEN_GROUP) + self._conf = config.Config(_base.AUTHTOKEN_GROUP, + opts.list_auth_token_opts(), + conf) super(AuthProtocol, self).__init__( app, log=log, - enforce_token_bind=self._conf_get('enforce_token_bind')) + enforce_token_bind=self._conf.get('enforce_token_bind')) # delay_auth_decision means we still allow unauthenticated requests # through and we let the downstream service make the final decision - self._delay_auth_decision = self._conf_get('delay_auth_decision') - self._include_service_catalog = self._conf_get( + self._delay_auth_decision = self._conf.get('delay_auth_decision') + self._include_service_catalog = self._conf.get( 'include_service_catalog') - self._hash_algorithms = self._conf_get('hash_algorithms') + self._hash_algorithms = self._conf.get('hash_algorithms') self._identity_server = self._create_identity_server() - self._auth_uri = self._conf_get('auth_uri') + self._auth_uri = self._conf.get('auth_uri') if not self._auth_uri: self.log.warning( _LW('Configuring auth_uri to point to the public identity ' @@ -714,30 +501,21 @@ class AuthProtocol(BaseAuthProtocol): self._auth_uri = self._identity_server.auth_uri self._signing_directory = _signing_dir.SigningDirectory( - directory_name=self._conf_get('signing_dir'), log=self.log) + directory_name=self._conf.get('signing_dir'), log=self.log) self._token_cache = self._token_cache_factory() revocation_cache_timeout = datetime.timedelta( - seconds=self._conf_get('revocation_cache_time')) + seconds=self._conf.get('revocation_cache_time')) self._revocations = _revocations.Revocations(revocation_cache_timeout, self._signing_directory, self._identity_server, self._cms_verify, self.log) - self._check_revocations_for_cached = self._conf_get( + self._check_revocations_for_cached = self._conf.get( 'check_revocations_for_cached') - def _conf_get(self, name, group=_base.AUTHTOKEN_GROUP): - # try config from paste-deploy first - if name in self._conf: - return self._conf[name] - elif self._local_oslo_config: - return self._local_oslo_config[group][name] - else: - return CONF[group][name] - def process_request(self, request): """Process request. @@ -994,30 +772,30 @@ class AuthProtocol(BaseAuthProtocol): def _get_auth_plugin(self): # NOTE(jamielennox): Ideally this would use load_from_conf_options # however that is not possible because we have to support the override - # pattern we use in _conf_get. This function therefore does a manual + # pattern we use in _conf.get. This function therefore does a manual # version of load_from_conf_options with the fallback plugin inline. - group = self._conf_get('auth_section') or _base.AUTHTOKEN_GROUP + group = self._conf.get('auth_section') or _base.AUTHTOKEN_GROUP - # NOTE(jamielennox): auth_plugin was deprecated to auth_type. _conf_get + # NOTE(jamielennox): auth_plugin was deprecated to auth_type. _conf.get # doesn't handle that deprecation in the case of conf dict options so # we have to manually check the value - plugin_name = (self._conf_get('auth_type', group=group) - or self._conf.get('auth_plugin')) + plugin_name = (self._conf.get('auth_type', group=group) + or self._conf.paste_overrides.get('auth_plugin')) if not plugin_name: return _auth.AuthTokenPlugin( log=self.log, - auth_admin_prefix=self._conf_get('auth_admin_prefix', + auth_admin_prefix=self._conf.get('auth_admin_prefix', group=group), - auth_host=self._conf_get('auth_host', group=group), - auth_port=self._conf_get('auth_port', group=group), - auth_protocol=self._conf_get('auth_protocol', group=group), - identity_uri=self._conf_get('identity_uri', group=group), - admin_token=self._conf_get('admin_token', group=group), - admin_user=self._conf_get('admin_user', group=group), - admin_password=self._conf_get('admin_password', group=group), - admin_tenant_name=self._conf_get('admin_tenant_name', + auth_host=self._conf.get('auth_host', group=group), + auth_port=self._conf.get('auth_port', group=group), + auth_protocol=self._conf.get('auth_protocol', group=group), + identity_uri=self._conf.get('identity_uri', group=group), + admin_token=self._conf.get('admin_token', group=group), + admin_user=self._conf.get('admin_user', group=group), + admin_password=self._conf.get('admin_password', group=group), + admin_tenant_name=self._conf.get('admin_tenant_name', group=group) ) @@ -1026,39 +804,12 @@ class AuthProtocol(BaseAuthProtocol): plugin_loader = loading.get_plugin_loader(plugin_name) plugin_opts = loading.get_auth_plugin_conf_options(plugin_loader) - (self._local_oslo_config or CONF).register_opts(plugin_opts, - group=group) - - getter = lambda opt: self._conf_get(opt.dest, group=group) + self._conf.oslo_conf_obj.register_opts(plugin_opts, group=group) + getter = lambda opt: self._conf.get(opt.dest, group=group) return plugin_loader.load_from_options_getter(getter) - def _determine_project(self): - """Determine a project name from all available config sources. - - The sources are checked in the following order: - - 1. The paste-deploy config for auth_token middleware - 2. The keystone_authtoken in the project's config - 3. The oslo.config CONF.project property - - """ - try: - return self._conf_get('project') - except cfg.NoSuchOptError: - # Prefer local oslo config object - if self._local_oslo_config: - return self._local_oslo_config.project - try: - # CONF.project will exist only if the service uses - # oslo.config. It will only be set when the project - # calls CONF(...) and when not set oslo.config oddly - # raises a NoSuchOptError exception. - return CONF.project - except cfg.NoSuchOptError: - return '' - def _build_useragent_string(self): - project = self._determine_project() + project = self._conf.project or '' if project: project_version = _get_project_version(project) project = '{project}/{project_version} '.format( @@ -1074,14 +825,14 @@ class AuthProtocol(BaseAuthProtocol): def _create_identity_server(self): # NOTE(jamielennox): Loading Session here should be exactly the # same as calling Session.load_from_conf_options(CONF, GROUP) - # however we can't do that because we have to use _conf_get to + # however we can't do that because we have to use _conf.get to # support the paste.ini options. sess = session_loading.Session().load_from_options( - cert=self._conf_get('certfile'), - key=self._conf_get('keyfile'), - cacert=self._conf_get('cafile'), - insecure=self._conf_get('insecure'), - timeout=self._conf_get('http_connect_timeout'), + cert=self._conf.get('certfile'), + key=self._conf.get('keyfile'), + cacert=self._conf.get('cafile'), + insecure=self._conf.get('insecure'), + timeout=self._conf.get('http_connect_timeout'), user_agent=self._build_useragent_string() ) @@ -1092,10 +843,10 @@ class AuthProtocol(BaseAuthProtocol): auth=auth_plugin, service_type='identity', interface='admin', - region_name=self._conf_get('region_name'), - connect_retries=self._conf_get('http_request_max_retries')) + region_name=self._conf.get('region_name'), + connect_retries=self._conf.get('http_request_max_retries')) - auth_version = self._conf_get('auth_version') + auth_version = self._conf.get('auth_version') if auth_version is not None: auth_version = discover.normalize_version_number(auth_version) return _identity.IdentityServer( @@ -1105,22 +856,22 @@ class AuthProtocol(BaseAuthProtocol): requested_auth_version=auth_version) def _token_cache_factory(self): - security_strategy = self._conf_get('memcache_security_strategy') + security_strategy = self._conf.get('memcache_security_strategy') cache_kwargs = dict( - cache_time=int(self._conf_get('token_cache_time')), - env_cache_name=self._conf_get('cache'), - memcached_servers=self._conf_get('memcached_servers'), - use_advanced_pool=self._conf_get('memcache_use_advanced_pool'), - dead_retry=self._conf_get('memcache_pool_dead_retry'), - maxsize=self._conf_get('memcache_pool_maxsize'), - unused_timeout=self._conf_get('memcache_pool_unused_timeout'), - conn_get_timeout=self._conf_get('memcache_pool_conn_get_timeout'), - socket_timeout=self._conf_get('memcache_pool_socket_timeout'), + cache_time=int(self._conf.get('token_cache_time')), + env_cache_name=self._conf.get('cache'), + memcached_servers=self._conf.get('memcached_servers'), + use_advanced_pool=self._conf.get('memcache_use_advanced_pool'), + dead_retry=self._conf.get('memcache_pool_dead_retry'), + maxsize=self._conf.get('memcache_pool_maxsize'), + unused_timeout=self._conf.get('memcache_pool_unused_timeout'), + conn_get_timeout=self._conf.get('memcache_pool_conn_get_timeout'), + socket_timeout=self._conf.get('memcache_pool_socket_timeout'), ) if security_strategy.lower() != 'none': - secret_key = self._conf_get('memcache_secret_key') + secret_key = self._conf.get('memcache_secret_key') return _cache.SecureTokenCache(self.log, security_strategy, secret_key, diff --git a/keystonemiddleware/auth_token/_auth.py b/keystonemiddleware/auth_token/_auth.py index 21b67893..f8317c45 100644 --- a/keystonemiddleware/auth_token/_auth.py +++ b/keystonemiddleware/auth_token/_auth.py @@ -14,7 +14,6 @@ import logging from keystoneauth1 import discover from keystoneauth1.identity import v2 -from keystoneauth1 import loading from keystoneauth1 import plugin from keystoneauth1 import token_endpoint from oslo_config import cfg @@ -188,5 +187,4 @@ OPTS = [ ] -loading.register_auth_conf_options(cfg.CONF, _base.AUTHTOKEN_GROUP) cfg.CONF.register_opts(OPTS, group=_base.AUTHTOKEN_GROUP) diff --git a/keystonemiddleware/auth_token/_exceptions.py b/keystonemiddleware/auth_token/_exceptions.py index be045c96..c0bf8dff 100644 --- a/keystonemiddleware/auth_token/_exceptions.py +++ b/keystonemiddleware/auth_token/_exceptions.py @@ -10,18 +10,19 @@ # License for the specific language governing permissions and limitations # under the License. +from keystonemiddleware import exceptions -class InvalidToken(Exception): + +ConfigurationError = exceptions.ConfigurationError + + +class InvalidToken(exceptions.KeystoneMiddlewareException): pass -class ServiceError(Exception): +class ServiceError(exceptions.KeystoneMiddlewareException): pass -class ConfigurationError(Exception): - pass - - -class RevocationListError(Exception): +class RevocationListError(exceptions.KeystoneMiddlewareException): pass diff --git a/keystonemiddleware/auth_token/_opts.py b/keystonemiddleware/auth_token/_opts.py index b1f57e98..52372eb7 100644 --- a/keystonemiddleware/auth_token/_opts.py +++ b/keystonemiddleware/auth_token/_opts.py @@ -13,14 +13,170 @@ import copy from keystoneauth1 import loading +from oslo_config import cfg -import keystonemiddleware.auth_token from keystonemiddleware.auth_token import _base + +# NOTE(jamielennox): A number of options below are deprecated however are left +# in the list and only mentioned as deprecated in the help string. This is +# because we have to provide the same deprecation functionality for arguments +# passed in via the conf in __init__ (from paste) and there is no way to test +# that the default value was set or not in CONF. +# Also if we were to remove the options from the CONF list (as typical CONF +# deprecation works) then other projects will not be able to override the +# options via CONF. + +_OPTS = [ + cfg.StrOpt('auth_uri', + default=None, + # FIXME(dolph): should be default='http://127.0.0.1:5000/v2.0/', + # or (depending on client support) an unversioned, publicly + # accessible identity endpoint (see bug 1207517). Further, we + # can eliminate this configuration option in favor of pulling + # the endpoint from the service catalog that the service user + # receives (there should be an identity endpoint listed there). + # This wasn't an option originally when many auth_token + # deployments were configured with the "ADMIN" token and + # endpoint combination. + help='Complete "public" Identity API endpoint. This endpoint' + ' should not be an "admin" endpoint, as it should be accessible' + ' by all end users. Unauthenticated clients are redirected to' + ' this endpoint to authenticate. Although this endpoint should ' + ' ideally be unversioned, client support in the wild varies. ' + ' If you\'re using a versioned v2 endpoint here, then this ' + ' should *not* be the same endpoint the service user utilizes ' + ' for validating tokens, because normal end users may not be ' + ' able to reach that endpoint.'), + cfg.StrOpt('auth_version', + default=None, + help='API version of the admin Identity API endpoint.'), + cfg.BoolOpt('delay_auth_decision', + default=False, + help='Do not handle authorization requests within the' + ' middleware, but delegate the authorization decision to' + ' downstream WSGI components.'), + cfg.IntOpt('http_connect_timeout', + default=None, + help='Request timeout value for communicating with Identity' + ' API server.'), + cfg.IntOpt('http_request_max_retries', + default=3, + help='How many times are we trying to reconnect when' + ' communicating with Identity API Server.'), + cfg.StrOpt('cache', + default=None, + help='Env key for the swift cache.'), + cfg.StrOpt('certfile', + help='Required if identity server requires client certificate'), + cfg.StrOpt('keyfile', + help='Required if identity server requires client certificate'), + cfg.StrOpt('cafile', default=None, + help='A PEM encoded Certificate Authority to use when ' + 'verifying HTTPs connections. Defaults to system CAs.'), + cfg.BoolOpt('insecure', default=False, help='Verify HTTPS connections.'), + cfg.StrOpt('region_name', default=None, + help='The region in which the identity server can be found.'), + cfg.StrOpt('signing_dir', + help='Directory used to cache files related to PKI tokens.'), + cfg.ListOpt('memcached_servers', + deprecated_name='memcache_servers', + help='Optionally specify a list of memcached server(s) to' + ' use for caching. If left undefined, tokens will instead be' + ' cached in-process.'), + cfg.IntOpt('token_cache_time', + default=300, + help='In order to prevent excessive effort spent validating' + ' tokens, the middleware caches previously-seen tokens for a' + ' configurable duration (in seconds). Set to -1 to disable' + ' caching completely.'), + cfg.IntOpt('revocation_cache_time', + default=10, + help='Determines the frequency at which the list of revoked' + ' tokens is retrieved from the Identity service (in seconds). A' + ' high number of revocation events combined with a low cache' + ' duration may significantly reduce performance. Only valid' + ' for PKI tokens.'), + cfg.StrOpt('memcache_security_strategy', + default='None', + choices=('None', 'MAC', 'ENCRYPT'), + ignore_case=True, + help='(Optional) If defined, indicate whether token data' + ' should be authenticated or authenticated and encrypted.' + ' If MAC, token data is authenticated (with HMAC) in the cache.' + ' If ENCRYPT, token data is encrypted and authenticated in the' + ' cache. If the value is not one of these options or empty,' + ' auth_token will raise an exception on initialization.'), + cfg.StrOpt('memcache_secret_key', + default=None, + secret=True, + help='(Optional, mandatory if memcache_security_strategy is' + ' defined) This string is used for key derivation.'), + cfg.IntOpt('memcache_pool_dead_retry', + default=5 * 60, + help='(Optional) Number of seconds memcached server is' + ' considered dead before it is tried again.'), + cfg.IntOpt('memcache_pool_maxsize', + default=10, + help='(Optional) Maximum total number of open connections to' + ' every memcached server.'), + cfg.IntOpt('memcache_pool_socket_timeout', + default=3, + help='(Optional) Socket timeout in seconds for communicating ' + 'with a memcached server.'), + cfg.IntOpt('memcache_pool_unused_timeout', + default=60, + help='(Optional) Number of seconds a connection to memcached' + ' is held unused in the pool before it is closed.'), + cfg.IntOpt('memcache_pool_conn_get_timeout', + default=10, + help='(Optional) Number of seconds that an operation will wait ' + 'to get a memcached client connection from the pool.'), + cfg.BoolOpt('memcache_use_advanced_pool', + default=False, + help='(Optional) Use the advanced (eventlet safe) memcached ' + 'client pool. The advanced pool will only work under ' + 'python 2.x.'), + cfg.BoolOpt('include_service_catalog', + default=True, + help='(Optional) Indicate whether to set the X-Service-Catalog' + ' header. If False, middleware will not ask for service' + ' catalog on token validation and will not set the' + ' X-Service-Catalog header.'), + cfg.StrOpt('enforce_token_bind', + default='permissive', + help='Used to control the use and type of token binding. Can' + ' be set to: "disabled" to not check token binding.' + ' "permissive" (default) to validate binding information if the' + ' bind type is of a form known to the server and ignore it if' + ' not. "strict" like "permissive" but if the bind type is' + ' unknown the token will be rejected. "required" any form of' + ' token binding is needed to be allowed. Finally the name of a' + ' binding method that must be present in tokens.'), + cfg.BoolOpt('check_revocations_for_cached', default=False, + help='If true, the revocation list will be checked for cached' + ' tokens. This requires that PKI tokens are configured on the' + ' identity server.'), + cfg.ListOpt('hash_algorithms', default=['md5'], + help='Hash algorithms to use for hashing PKI tokens. This may' + ' be a single algorithm or multiple. The algorithms are those' + ' supported by Python standard hashlib.new(). The hashes will' + ' be tried in the order given, so put the preferred one first' + ' for performance. The result of the first hash will be stored' + ' in the cache. This will typically be set to multiple values' + ' only while migrating from a less secure algorithm to a more' + ' secure one. Once all the old tokens are expired this option' + ' should be set to a single value for better performance.'), +] + + +CONF = cfg.CONF +CONF.register_opts(_OPTS, group=_base.AUTHTOKEN_GROUP) +loading.register_auth_conf_options(cfg.CONF, _base.AUTHTOKEN_GROUP) + + auth_token_opts = [ - (_base.AUTHTOKEN_GROUP, - keystonemiddleware.auth_token._OPTS + - loading.get_auth_common_conf_options()) + (_base.AUTHTOKEN_GROUP, _OPTS + loading.get_auth_common_conf_options()), ] __all__ = ( @@ -49,7 +205,6 @@ def list_opts(): :returns: a list of (group_name, opts) tuples """ - auth_token_opts = (keystonemiddleware.auth_token._OPTS + - loading.get_auth_common_conf_options()) + auth_token_opts = (_OPTS + loading.get_auth_common_conf_options()) return [(_base.AUTHTOKEN_GROUP, copy.deepcopy(auth_token_opts))] diff --git a/keystonemiddleware/exceptions.py b/keystonemiddleware/exceptions.py new file mode 100644 index 00000000..8a476e9d --- /dev/null +++ b/keystonemiddleware/exceptions.py @@ -0,0 +1,19 @@ +# 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. + + +class KeystoneMiddlewareException(Exception): + pass + + +class ConfigurationError(KeystoneMiddlewareException): + pass diff --git a/keystonemiddleware/opts.py b/keystonemiddleware/opts.py index 400ad769..d563da89 100644 --- a/keystonemiddleware/opts.py +++ b/keystonemiddleware/opts.py @@ -20,15 +20,13 @@ import copy from keystoneauth1 import loading -import keystonemiddleware.auth_token from keystonemiddleware.auth_token import _auth from keystonemiddleware.auth_token import _base +from keystonemiddleware.auth_token import _opts auth_token_opts = [ (_base.AUTHTOKEN_GROUP, - keystonemiddleware.auth_token._OPTS + - _auth.OPTS + - loading.get_auth_common_conf_options()) + _opts._OPTS + _auth.OPTS + loading.get_auth_common_conf_options()) ] diff --git a/keystonemiddleware/tests/unit/auth_token/test_auth_token_middleware.py b/keystonemiddleware/tests/unit/auth_token/test_auth_token_middleware.py index 223ec9e8..979dbe30 100644 --- a/keystonemiddleware/tests/unit/auth_token/test_auth_token_middleware.py +++ b/keystonemiddleware/tests/unit/auth_token/test_auth_token_middleware.py @@ -33,7 +33,6 @@ import mock from oslo_config import cfg from oslo_serialization import jsonutils from oslo_utils import timeutils -from oslotest import createfile import six import testresources import testtools @@ -499,7 +498,7 @@ class GeneralAuthTokenMiddlewareTest(BaseAuthTokenMiddlewareTest, self.assertEqual(datetime.timedelta(seconds=24), middleware._revocations._cache_timeout) self.assertEqual(False, middleware._include_service_catalog) - self.assertEqual('0', middleware._conf['nonexsit_option']) + self.assertEqual('0', middleware._conf.get('nonexsit_option')) def test_deprecated_conf_values(self): servers = 'localhost:11211' @@ -509,7 +508,7 @@ class GeneralAuthTokenMiddlewareTest(BaseAuthTokenMiddlewareTest, } middleware = auth_token.AuthProtocol(self.fake_app, conf) - self.assertEqual([servers], middleware._conf_get('memcached_servers')) + self.assertEqual([servers], middleware._conf.get('memcached_servers')) def test_conf_values_type_convert_with_wrong_value(self): conf = { @@ -2534,86 +2533,5 @@ class TestAuthPluginUserAgentGeneration(BaseAuthTokenMiddlewareTest): self.assertThat(sess.user_agent, matchers.StartsWith(expected_ua)) -class TestAuthPluginLocalOsloConfig(BaseAuthTokenMiddlewareTest): - - def setUp(self): - super(TestAuthPluginLocalOsloConfig, self).setUp() - self.project = uuid.uuid4().hex - - # NOTE(cdent): The options below are selected from those - # which are statically registered by auth_token middleware - # in the 'keystone_authtoken' group. Additional options, from - # plugins, are registered dynamically so must not be used here. - self.oslo_options = { - 'auth_uri': uuid.uuid4().hex, - 'identity_uri': uuid.uuid4().hex, - } - - self.local_oslo_config = cfg.ConfigOpts() - self.local_oslo_config.register_group(cfg.OptGroup( - name='keystone_authtoken')) - self.local_oslo_config.register_opts(auth_token._OPTS, - group='keystone_authtoken') - self.local_oslo_config.register_opts(auth_token._auth.OPTS, - group='keystone_authtoken') - for option, value in self.oslo_options.items(): - self.local_oslo_config.set_override(option, value, - 'keystone_authtoken') - self.local_oslo_config(args=[], project=self.project) - - self.file_options = { - 'auth_type': 'password', - 'auth_uri': uuid.uuid4().hex, - 'password': uuid.uuid4().hex, - } - - content = ("[keystone_authtoken]\n" - "auth_type=%(auth_type)s\n" - "auth_uri=%(auth_uri)s\n" - "auth_url=%(auth_uri)s\n" - "password=%(password)s\n" % self.file_options) - self.conf_file_fixture = self.useFixture( - createfile.CreateFileWithContent(self.project, content)) - - def test_project_in_local_oslo_configuration(self): - conf = {'oslo_config_project': self.project, - 'oslo_config_file': self.conf_file_fixture.path} - app = self._create_app(conf, uuid.uuid4().hex) - for option in self.file_options: - self.assertEqual(self.file_options[option], - app._conf_get(option), option) - - def test_passed_oslo_configuration(self): - conf = {'oslo_config_config': self.local_oslo_config} - app = self._create_app(conf, uuid.uuid4().hex) - for option in self.oslo_options: - self.assertEqual(self.oslo_options[option], - app._conf_get(option)) - - def test_passed_olso_configuration_wins(self): - """oslo_config_config has precedence over oslo_config_project.""" - conf = {'oslo_config_project': self.project, - 'oslo_config_config': self.local_oslo_config, - 'oslo_config_file': self.conf_file_fixture.path} - app = self._create_app(conf, uuid.uuid4().hex) - for option in self.oslo_options: - self.assertEqual(self.oslo_options[option], - app._conf_get(option)) - self.assertNotEqual(self.file_options['auth_uri'], - app._conf_get('auth_uri')) - - def _create_app(self, conf, project_version): - fake_pkg_resources = mock.Mock() - fake_pkg_resources.get_distribution().version = project_version - - body = uuid.uuid4().hex - with mock.patch('keystonemiddleware.auth_token.pkg_resources', - new=fake_pkg_resources): - # use_global_conf is poorly named. What it means is - # don't use the config created in test setUp. - return self.create_simple_middleware(body=body, conf=conf, - use_global_conf=True) - - def load_tests(loader, tests, pattern): return testresources.OptimisingTestSuite(tests) diff --git a/keystonemiddleware/tests/unit/auth_token/test_config.py b/keystonemiddleware/tests/unit/auth_token/test_config.py new file mode 100644 index 00000000..40e39535 --- /dev/null +++ b/keystonemiddleware/tests/unit/auth_token/test_config.py @@ -0,0 +1,112 @@ +# 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 uuid + +import mock +from oslo_config import cfg +from oslotest import createfile + +from keystonemiddleware.auth_token import _auth +from keystonemiddleware.auth_token import _opts +from keystonemiddleware.tests.unit.auth_token import base + + +def conf_get(app, *args, **kwargs): + return app._conf.get(*args, **kwargs) + + +class TestAuthPluginLocalOsloConfig(base.BaseAuthTokenTestCase): + + def setUp(self): + super(TestAuthPluginLocalOsloConfig, self).setUp() + self.project = uuid.uuid4().hex + + # NOTE(cdent): The options below are selected from those + # which are statically registered by auth_token middleware + # in the 'keystone_authtoken' group. Additional options, from + # plugins, are registered dynamically so must not be used here. + self.oslo_options = { + 'auth_uri': uuid.uuid4().hex, + 'identity_uri': uuid.uuid4().hex, + } + + self.local_oslo_config = cfg.ConfigOpts() + self.local_oslo_config.register_group( + cfg.OptGroup(name='keystone_authtoken')) + + self.local_oslo_config.register_opts(_opts._OPTS, + group='keystone_authtoken') + self.local_oslo_config.register_opts(_auth.OPTS, + group='keystone_authtoken') + + for option, value in self.oslo_options.items(): + self.local_oslo_config.set_override(option, value, + 'keystone_authtoken') + self.local_oslo_config(args=[], project=self.project) + + self.file_options = { + 'auth_type': 'password', + 'auth_uri': uuid.uuid4().hex, + 'password': uuid.uuid4().hex, + } + + content = ("[keystone_authtoken]\n" + "auth_type=%(auth_type)s\n" + "auth_uri=%(auth_uri)s\n" + "auth_url=%(auth_uri)s\n" + "password=%(password)s\n" % self.file_options) + + self.conf_file_fixture = self.useFixture( + createfile.CreateFileWithContent(self.project, content)) + + def _create_app(self, conf, project_version=None): + if not project_version: + project_version = uuid.uuid4().hex + + fake_pkg_resources = mock.Mock() + fake_pkg_resources.get_distribution().version = project_version + + body = uuid.uuid4().hex + with mock.patch('keystonemiddleware.auth_token.pkg_resources', + new=fake_pkg_resources): + # use_global_conf is poorly named. What it means is + # don't use the config created in test setUp. + return self.create_simple_middleware(body=body, conf=conf, + use_global_conf=True) + + def test_project_in_local_oslo_configuration(self): + conf = {'oslo_config_project': self.project, + 'oslo_config_file': self.conf_file_fixture.path} + app = self._create_app(conf) + for option in self.file_options: + self.assertEqual(self.file_options[option], + conf_get(app, option), option) + + def test_passed_oslo_configuration(self): + conf = {'oslo_config_config': self.local_oslo_config} + app = self._create_app(conf) + for option in self.oslo_options: + self.assertEqual(self.oslo_options[option], + conf_get(app, option)) + + def test_passed_olso_configuration_wins(self): + """oslo_config_config has precedence over oslo_config_project.""" + conf = {'oslo_config_project': self.project, + 'oslo_config_config': self.local_oslo_config, + 'oslo_config_file': self.conf_file_fixture.path} + app = self._create_app(conf) + for option in self.oslo_options: + self.assertEqual(self.oslo_options[option], + conf_get(app, option)) + self.assertNotEqual(self.file_options['auth_uri'], + conf_get(app, 'auth_uri'))