diff options
Diffstat (limited to 'keystonemiddleware-moon/keystonemiddleware/auth_token')
13 files changed, 0 insertions, 3007 deletions
diff --git a/keystonemiddleware-moon/keystonemiddleware/auth_token/__init__.py b/keystonemiddleware-moon/keystonemiddleware/auth_token/__init__.py deleted file mode 100644 index be268da3..00000000 --- a/keystonemiddleware-moon/keystonemiddleware/auth_token/__init__.py +++ /dev/null @@ -1,1129 +0,0 @@ -# Copyright 2010-2012 OpenStack Foundation -# -# 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. - -""" -Token-based Authentication Middleware - -This WSGI component: - -* Verifies that incoming client requests have valid tokens by validating - tokens with the auth service. -* Rejects unauthenticated requests unless the auth_token middleware is in - ``delay_auth_decision`` mode, which means the final decision is delegated to - the downstream WSGI component (usually the OpenStack service). -* Collects and forwards identity information based on a valid token - such as user name, domain, project, etc. - -Refer to: http://docs.openstack.org/developer/keystonemiddleware/\ -middlewarearchitecture.html - - -Headers -------- - -The auth_token middleware uses headers sent in by the client on the request -and sets headers and environment variables for the downstream WSGI component. - -Coming in from initial call from client or customer -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -HTTP_X_AUTH_TOKEN - The client token being passed in. - -HTTP_X_SERVICE_TOKEN - A service token being passed in. - -Used for communication between components -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -WWW-Authenticate - HTTP header returned to a user indicating which endpoint to use - to retrieve a new token. - -What auth_token adds to the request for use by the OpenStack service -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -When using composite authentication (a user and service token are -present) additional service headers relating to the service user -will be added. They take the same form as the standard headers but add -``_SERVICE_``. These headers will not exist in the environment if no -service token is present. - -HTTP_X_IDENTITY_STATUS, HTTP_X_SERVICE_IDENTITY_STATUS - Will be set to either ``Confirmed`` or ``Invalid``. - - The underlying service will only see a value of 'Invalid' if the middleware - is configured to run in ``delay_auth_decision`` mode. As with all such - headers, ``HTTP_X_SERVICE_IDENTITY_STATUS`` will only exist in the - environment if a service token is presented. This is different than - ``HTTP_X_IDENTITY_STATUS`` which is always set even if no user token is - presented. This allows the underlying service to determine if a - denial should use ``401 Unauthenticated`` or ``403 Forbidden``. - -HTTP_X_DOMAIN_ID, HTTP_X_SERVICE_DOMAIN_ID - Identity service managed unique identifier, string. Only present if - this is a domain-scoped token. - -HTTP_X_DOMAIN_NAME, HTTP_X_SERVICE_DOMAIN_NAME - Unique domain name, string. Only present if this is a domain-scoped - token. - -HTTP_X_PROJECT_ID, HTTP_X_SERVICE_PROJECT_ID - Identity service managed unique identifier, string. Only present if - this is a project-scoped token. - -HTTP_X_PROJECT_NAME, HTTP_X_SERVICE_PROJECT_NAME - Project name, unique within owning domain, string. Only present if - this is a project-scoped token. - -HTTP_X_PROJECT_DOMAIN_ID, HTTP_X_SERVICE_PROJECT_DOMAIN_ID - Identity service managed unique identifier of owning domain of - project, string. Only present if this is a project-scoped v3 token. If - this variable is set, this indicates that the PROJECT_NAME can only - be assumed to be unique within this domain. - -HTTP_X_PROJECT_DOMAIN_NAME, HTTP_X_SERVICE_PROJECT_DOMAIN_NAME - Name of owning domain of project, string. Only present if this is a - project-scoped v3 token. If this variable is set, this indicates that - the PROJECT_NAME can only be assumed to be unique within this domain. - -HTTP_X_USER_ID, HTTP_X_SERVICE_USER_ID - Identity-service managed unique identifier, string. - -HTTP_X_USER_NAME, HTTP_X_SERVICE_USER_NAME - User identifier, unique within owning domain, string. - -HTTP_X_USER_DOMAIN_ID, HTTP_X_SERVICE_USER_DOMAIN_ID - Identity service managed unique identifier of owning domain of - user, string. If this variable is set, this indicates that the USER_NAME - can only be assumed to be unique within this domain. - -HTTP_X_USER_DOMAIN_NAME, HTTP_X_SERVICE_USER_DOMAIN_NAME - Name of owning domain of user, string. If this variable is set, this - indicates that the USER_NAME can only be assumed to be unique within - this domain. - -HTTP_X_ROLES, HTTP_X_SERVICE_ROLES - Comma delimited list of case-sensitive role names. - -HTTP_X_SERVICE_CATALOG - service catalog (optional, JSON string). - - For compatibility reasons this catalog will always be in the V2 catalog - format even if it is a v3 token. - - .. note:: This is an exception in that it contains 'SERVICE' but relates to - a user token, not a service token. The existing user's catalog can be - very large; it was decided not to present a catalog relating to the - service token to avoid using more HTTP header space. - -HTTP_X_TENANT_ID - *Deprecated* in favor of HTTP_X_PROJECT_ID. - - Identity service managed unique identifier, string. For v3 tokens, this - will be set to the same value as HTTP_X_PROJECT_ID. - -HTTP_X_TENANT_NAME - *Deprecated* in favor of HTTP_X_PROJECT_NAME. - - Project identifier, unique within owning domain, string. For v3 tokens, - this will be set to the same value as HTTP_X_PROJECT_NAME. - -HTTP_X_TENANT - *Deprecated* in favor of HTTP_X_TENANT_ID and HTTP_X_TENANT_NAME. - - Identity server-assigned unique identifier, string. For v3 tokens, this - will be set to the same value as HTTP_X_PROJECT_ID. - -HTTP_X_USER - *Deprecated* in favor of HTTP_X_USER_ID and HTTP_X_USER_NAME. - - User name, unique within owning domain, string. - -HTTP_X_ROLE - *Deprecated* in favor of HTTP_X_ROLES. - - Will contain the same values as HTTP_X_ROLES. - -Environment Variables -^^^^^^^^^^^^^^^^^^^^^ - -These variables are set in the request environment for use by the downstream -WSGI component. - -keystone.token_info - Information about the token discovered in the process of validation. This - may include extended information returned by the token validation call, as - well as basic information about the project and user. - -keystone.token_auth - A keystoneclient auth plugin that may be used with a - :py:class:`keystoneclient.session.Session`. This plugin will load the - authentication data provided to auth_token middleware. - - -Configuration -------------- - -auth_token middleware configuration can be in the main application's -configuration file, e.g. in ``nova.conf``: - -.. code-block:: ini - - [keystone_authtoken] - auth_plugin = password - auth_url = http://keystone:35357/ - username = nova - user_domain_id = default - password = whyarewestillusingpasswords - project_name = service - project_domain_id = default - -Configuration can also be in the ``api-paste.ini`` file with the same options, -but this is discouraged. - -Swift ------ - -When deploy auth_token middleware with Swift, user may elect to use Swift -memcache instead of the local auth_token memcache. Swift memcache is passed in -from the request environment and it's identified by the ``swift.cache`` key. -However it could be different, depending on deployment. To use Swift memcache, -you must set the ``cache`` option to the environment key where the Swift cache -object is stored. - -""" - -import binascii -import datetime -import logging - -from keystoneclient import access -from keystoneclient import adapter -from keystoneclient import auth -from keystoneclient.common import cms -from keystoneclient import discover -from keystoneclient import exceptions -from keystoneclient import session -from oslo_config import cfg -from oslo_serialization import jsonutils -import pkg_resources -import six -import webob.dec - -from keystonemiddleware.auth_token import _auth -from keystonemiddleware.auth_token import _base -from keystonemiddleware.auth_token import _cache -from keystonemiddleware.auth_token import _exceptions as exc -from keystonemiddleware.auth_token import _identity -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.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) - help='Complete public Identity API 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.'), - cfg.StrOpt('memcache_security_strategy', - default=None, - help='(Optional) If defined, indicate whether token data' - ' should be authenticated or authenticated and encrypted.' - ' Acceptable values are MAC or ENCRYPT. 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__) - - -class _BIND_MODE(object): - DISABLED = 'disabled' - PERMISSIVE = 'permissive' - STRICT = 'strict' - REQUIRED = 'required' - KERBEROS = 'kerberos' - - -def _token_is_v2(token_info): - return ('access' in token_info) - - -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.AuthTokenPlugin.get_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 - - 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: - # This option is not known to auth_token. - pass - except ValueError as e: - raise exc.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): - return pkg_resources.get_distribution(project).version - - -class _BaseAuthProtocol(object): - """A base class for AuthProtocol token checking implementations. - - :param Callable app: The next application to call after middleware. - :param logging.Logger log: The logging object to use for output. By default - it will use a logger in the - keystonemiddleware.auth_token namespace. - :param str enforce_token_bind: The style of token binding enforcement to - perform. - """ - - def __init__(self, - app, - log=_LOG, - enforce_token_bind=_BIND_MODE.PERMISSIVE): - self.log = log - self._app = app - self._enforce_token_bind = enforce_token_bind - - @webob.dec.wsgify(RequestClass=_request._AuthTokenRequest) - def __call__(self, req): - """Handle incoming request.""" - response = self.process_request(req) - if response: - return response - response = req.get_response(self._app) - return self.process_response(response) - - def process_request(self, request): - """Process request. - - If this method returns a value then that value will be used as the - response. The next application down the stack will not be executed and - process_response will not be called. - - Otherwise, the next application down the stack will be executed and - process_response will be called with the generated response. - - By default this method does not return a value. - - :param request: Incoming request - :type request: _request.AuthTokenRequest - - """ - request.remove_auth_headers() - - user_auth_ref = None - serv_auth_ref = None - - if request.user_token: - self.log.debug('Authenticating user token') - try: - data, user_auth_ref = self._do_fetch_token(request.user_token) - self._validate_token(user_auth_ref) - self._confirm_token_bind(user_auth_ref, request) - except exc.InvalidToken: - self.log.info(_LI('Invalid user token')) - request.user_token_valid = False - else: - request.user_token_valid = True - request.environ['keystone.token_info'] = data - - if request.service_token: - self.log.debug('Authenticating service token') - try: - _, serv_auth_ref = self._do_fetch_token(request.service_token) - self._validate_token(serv_auth_ref) - self._confirm_token_bind(serv_auth_ref, request) - except exc.InvalidToken: - self.log.info(_LI('Invalid service token')) - request.service_token_valid = False - else: - request.service_token_valid = True - - p = _user_plugin.UserAuthPlugin(user_auth_ref, serv_auth_ref) - request.environ['keystone.token_auth'] = p - - def _validate_token(self, auth_ref): - """Perform the validation steps on the token. - - :param auth_ref: The token data - :type auth_ref: keystoneclient.access.AccessInfo - - :raises exc.InvalidToken: if token is rejected - """ - # 0 seconds of validity means is it valid right now. - if auth_ref.will_expire_soon(stale_duration=0): - raise exc.InvalidToken(_('Token authorization failed')) - - def _do_fetch_token(self, token): - """Helper method to fetch a token and convert it into an AccessInfo""" - data = self._fetch_token(token) - - try: - return data, access.AccessInfo.factory(body=data, auth_token=token) - except Exception: - self.log.warning(_LW('Invalid token contents.'), exc_info=True) - raise exc.InvalidToken(_('Token authorization failed')) - - def _fetch_token(self, token): - """Fetch the token data based on the value in the header. - - Retrieve the data associated with the token value that was in the - header. This can be from PKI, contacting the identity server or - whatever is required. - - :param str token: The token present in the request header. - - :raises exc.InvalidToken: if token is invalid. - - :returns: The token data - :rtype: dict - """ - raise NotImplemented() - - def process_response(self, response): - """Do whatever you'd like to the response. - - By default the response is returned unmodified. - - :param response: Response object - :type response: ._request._AuthTokenResponse - """ - return response - - def _invalid_user_token(self, msg=False): - # NOTE(jamielennox): use False as the default so that None is valid - if msg is False: - msg = _('Token authorization failed') - - raise exc.InvalidToken(msg) - - def _confirm_token_bind(self, auth_ref, req): - if self._enforce_token_bind == _BIND_MODE.DISABLED: - return - - try: - if auth_ref.version == 'v2.0': - bind = auth_ref['token']['bind'] - elif auth_ref.version == 'v3': - bind = auth_ref['bind'] - else: - self._invalid_user_token() - except KeyError: - bind = {} - - # permissive and strict modes don't require there to be a bind - permissive = self._enforce_token_bind in (_BIND_MODE.PERMISSIVE, - _BIND_MODE.STRICT) - - if not bind: - if permissive: - # no bind provided and none required - return - else: - self.log.info(_LI('No bind information present in token.')) - self._invalid_user_token() - - # get the named mode if bind_mode is not one of the predefined - if permissive or self._enforce_token_bind == _BIND_MODE.REQUIRED: - name = None - else: - name = self._enforce_token_bind - - if name and name not in bind: - self.log.info(_LI('Named bind mode %s not in bind information'), - name) - self._invalid_user_token() - - for bind_type, identifier in six.iteritems(bind): - if bind_type == _BIND_MODE.KERBEROS: - if req.auth_type != 'negotiate': - self.log.info(_LI('Kerberos credentials required and ' - 'not present.')) - self._invalid_user_token() - - if req.remote_user != identifier: - self.log.info(_LI('Kerberos credentials do not match ' - 'those in bind.')) - self._invalid_user_token() - - self.log.debug('Kerberos bind authentication successful.') - - elif self._enforce_token_bind == _BIND_MODE.PERMISSIVE: - self.log.debug('Ignoring Unknown bind for permissive mode: ' - '%(bind_type)s: %(identifier)s.', - {'bind_type': bind_type, - 'identifier': identifier}) - - else: - self.log.info( - _LI('Couldn`t verify unknown bind: %(bind_type)s: ' - '%(identifier)s.'), - {'bind_type': bind_type, 'identifier': identifier}) - self._invalid_user_token() - - -class AuthProtocol(_BaseAuthProtocol): - """Middleware that handles authenticating client calls.""" - - _SIGNING_CERT_FILE_NAME = 'signing_cert.pem' - _SIGNING_CA_FILE_NAME = 'cacert.pem' - - def __init__(self, app, conf): - 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): If we don't want to use oslo.config global object - # we can set the paste "oslo_config_project" and the middleware - # will load the configuration with a local oslo.config object. - self._local_oslo_config = None - if 'oslo_config_project' in conf: - if 'oslo_config_file' in conf: - default_config_files = [conf['oslo_config_file']] - else: - default_config_files = None - - # For unit tests, support passing in a ConfigOpts in - # oslo_config_config. - self._local_oslo_config = conf.get('oslo_config_config', - cfg.ConfigOpts()) - self._local_oslo_config( - {}, project=conf['oslo_config_project'], - default_config_files=default_config_files, - validate_default_values=True) - - self._local_oslo_config.register_opts( - _OPTS, group=_base.AUTHTOKEN_GROUP) - auth.register_conf_options(self._local_oslo_config, - group=_base.AUTHTOKEN_GROUP) - - super(AuthProtocol, self).__init__( - app, - log=log, - 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( - 'include_service_catalog') - self._hash_algorithms = self._conf_get('hash_algorithms') - - self._identity_server = self._create_identity_server() - - 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 ' - 'endpoint is required; clients may not be able to ' - 'authenticate against an admin endpoint')) - - # FIXME(dolph): drop support for this fallback behavior as - # documented in bug 1207517. - - self._auth_uri = self._identity_server.auth_uri - - self._signing_directory = _signing_dir.SigningDirectory( - 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')) - 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( - '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. - - Evaluate the headers in a request and attempt to authenticate the - request. If authenticated then additional headers are added to the - request for use by applications. If not authenticated the request will - be rejected or marked unauthenticated depending on configuration. - """ - self._token_cache.initialize(request.environ) - - resp = super(AuthProtocol, self).process_request(request) - if resp: - return resp - - if not request.user_token: - # if no user token is present then that's an invalid request - request.user_token_valid = False - - # NOTE(jamielennox): The service status is allowed to be missing if a - # service token is not passed. If the service status is missing that's - # a valid request. We should find a better way to expose this from the - # request object. - user_status = request.user_token and request.user_token_valid - service_status = request.headers.get('X-Service-Identity-Status', - 'Confirmed') - - if not (user_status and service_status == 'Confirmed'): - if self._delay_auth_decision: - self.log.info(_LI('Deferring reject downstream')) - else: - self.log.info(_LI('Rejecting request')) - self._reject_request() - - if request.user_token_valid: - request.set_user_headers(request.token_auth._user_auth_ref, - self._include_service_catalog) - - if request.service_token and request.service_token_valid: - request.set_service_headers(request.token_auth._serv_auth_ref) - - if self.log.isEnabledFor(logging.DEBUG): - self.log.debug('Received request from %s', - request.token_auth._log_format) - - def process_response(self, response): - """Process Response. - - Add ``WWW-Authenticate`` headers to requests that failed with - ``401 Unauthenticated`` so users know where to authenticate for future - requests. - """ - if response.status_int == 401: - response.headers.extend(self._reject_auth_headers) - - return response - - @property - def _reject_auth_headers(self): - header_val = 'Keystone uri=\'%s\'' % self._auth_uri - return [('WWW-Authenticate', header_val)] - - def _reject_request(self): - """Redirect client to auth server. - - :param env: wsgi request environment - :param start_response: wsgi response callback - :returns: HTTPUnauthorized http response - - """ - raise webob.exc.HTTPUnauthorized(body='Authentication required', - headers=self._reject_auth_headers) - - def _token_hashes(self, token): - """Generate a list of hashes that the current token may be cached as. - - With PKI tokens we have multiple hashing algorithms that we test with - revocations. This generates that whole list. - - The first element of this list is the preferred algorithm and is what - new cache values should be saved as. - - :param str token: The token being presented by a user. - - :returns: list of str token hashes. - """ - if cms.is_asn1_token(token) or cms.is_pkiz(token): - return list(cms.cms_hash_token(token, mode=algo) - for algo in self._hash_algorithms) - else: - return [token] - - def _cache_get_hashes(self, token_hashes): - """Check if the token is cached already. - - Functions takes a list of hashes that might be in the cache and matches - the first one that is present. If nothing is found in the cache it - returns None. - - :returns: token data if found else None. - """ - - for token in token_hashes: - cached = self._token_cache.get(token) - - if cached: - return cached - - def _fetch_token(self, token): - """Retrieve a token from either a PKI bundle or the identity server. - - :param str token: token id - - :raises exc.InvalidToken: if token is rejected - """ - data = None - token_hashes = None - - try: - token_hashes = self._token_hashes(token) - cached = self._cache_get_hashes(token_hashes) - - if cached: - data = cached - - if self._check_revocations_for_cached: - # A token stored in Memcached might have been revoked - # regardless of initial mechanism used to validate it, - # and needs to be checked. - self._revocations.check(token_hashes) - else: - data = self._validate_offline(token, token_hashes) - if not data: - data = self._identity_server.verify_token(token) - - self._token_cache.store(token_hashes[0], data) - - except (exceptions.ConnectionRefused, exceptions.RequestTimeout, - exc.RevocationListError, exc.ServiceError) as e: - self.log.critical(_LC('Unable to validate token: %s'), e) - raise webob.exc.HTTPServiceUnavailable() - except exc.InvalidToken: - self.log.debug('Token validation failure.', exc_info=True) - if token_hashes: - self._token_cache.store_invalid(token_hashes[0]) - self.log.warning(_LW('Authorization failed for token')) - raise - except Exception: - self.log.critical(_LC('Unable to validate token'), exc_info=True) - raise webob.exc.HTTPInternalServerError() - - return data - - def _validate_offline(self, token, token_hashes): - try: - if cms.is_pkiz(token): - verified = self._verify_pkiz_token(token, token_hashes) - elif cms.is_asn1_token(token): - verified = self._verify_signed_token(token, token_hashes) - else: - # Can't do offline validation for this type of token. - return - except exceptions.CertificateConfigError: - self.log.warning(_LW('Fetch certificate config failed, ' - 'fallback to online validation.')) - except exc.RevocationListError: - self.log.warning(_LW('Fetch revocation list failed, ' - 'fallback to online validation.')) - else: - data = jsonutils.loads(verified) - - audit_ids = None - if 'access' in data: - # It's a v2 token. - audit_ids = data['access']['token'].get('audit_ids') - else: - # It's a v3 token - audit_ids = data['token'].get('audit_ids') - - if audit_ids: - self._revocations.check_by_audit_id(audit_ids) - - return data - - def _validate_token(self, auth_ref): - super(AuthProtocol, self)._validate_token(auth_ref) - - if auth_ref.version == 'v2.0' and not auth_ref.project_id: - msg = _('Unable to determine service tenancy.') - raise exc.InvalidToken(msg) - - def _cms_verify(self, data, inform=cms.PKI_ASN1_FORM): - """Verifies the signature of the provided data's IAW CMS syntax. - - If either of the certificate files might be missing, fetch them and - retry. - """ - def verify(): - try: - signing_cert_path = self._signing_directory.calc_path( - self._SIGNING_CERT_FILE_NAME) - signing_ca_path = self._signing_directory.calc_path( - self._SIGNING_CA_FILE_NAME) - return cms.cms_verify(data, signing_cert_path, - signing_ca_path, - inform=inform).decode('utf-8') - except (exceptions.CMSError, - cms.subprocess.CalledProcessError) as err: - self.log.warning(_LW('Verify error: %s'), err) - raise exc.InvalidToken(_('Token authorization failed')) - - try: - return verify() - except exceptions.CertificateConfigError: - # the certs might be missing; unconditionally fetch to avoid racing - self._fetch_signing_cert() - self._fetch_ca_cert() - - try: - # retry with certs in place - return verify() - except exceptions.CertificateConfigError as err: - # if this is still occurring, something else is wrong and we - # need err.output to identify the problem - self.log.error(_LE('CMS Verify output: %s'), err.output) - raise - - def _verify_signed_token(self, signed_text, token_ids): - """Check that the token is unrevoked and has a valid signature.""" - self._revocations.check(token_ids) - formatted = cms.token_to_cms(signed_text) - verified = self._cms_verify(formatted) - return verified - - def _verify_pkiz_token(self, signed_text, token_ids): - self._revocations.check(token_ids) - try: - uncompressed = cms.pkiz_uncompress(signed_text) - verified = self._cms_verify(uncompressed, inform=cms.PKIZ_CMS_FORM) - return verified - # TypeError If the signed_text is not zlib compressed - # binascii.Error if signed_text has incorrect base64 padding (py34) - except (TypeError, binascii.Error): - raise exc.InvalidToken(signed_text) - - def _fetch_signing_cert(self): - self._signing_directory.write_file( - self._SIGNING_CERT_FILE_NAME, - self._identity_server.fetch_signing_cert()) - - def _fetch_ca_cert(self): - self._signing_directory.write_file( - self._SIGNING_CA_FILE_NAME, - self._identity_server.fetch_ca_cert()) - - def _get_auth_plugin(self): - # NOTE(jamielennox): Ideally this would use get_from_conf_options - # however that is not possible because we have to support the override - # pattern we use in _conf_get. There is a somewhat replacement for this - # in keystoneclient in load_from_options_getter which should be used - # when available. Until then this is essentially a copy and paste of - # the ksc load_from_conf_options code because we need to get a fix out - # for this quickly. - - # FIXME(jamielennox): update to use load_from_options_getter when - # https://review.openstack.org/162529 merges. - - # !!! - UNDER NO CIRCUMSTANCES COPY ANY OF THIS CODE - !!! - - group = self._conf_get('auth_section') or _base.AUTHTOKEN_GROUP - plugin_name = self._conf_get('auth_plugin', group=group) - plugin_kwargs = dict() - - if plugin_name: - plugin_class = auth.get_plugin_class(plugin_name) - else: - plugin_class = _auth.AuthTokenPlugin - # logger object is a required parameter of the default plugin - plugin_kwargs['log'] = self.log - - plugin_opts = plugin_class.get_options() - (self._local_oslo_config or CONF).register_opts(plugin_opts, - group=group) - - for opt in plugin_opts: - val = self._conf_get(opt.dest, group=group) - if val is not None: - val = opt.type(val) - plugin_kwargs[opt.dest] = val - - return plugin_class.load_from_options(**plugin_kwargs) - - 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() - if project: - project_version = _get_project_version(project) - project = '{project}/{project_version} '.format( - project=project, - project_version=project_version) - - ua_template = ('{project}' - 'keystonemiddleware.auth_token/{ksm_version}') - return ua_template.format( - project=project, - ksm_version=_get_project_version('keystonemiddleware')) - - 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 - # support the paste.ini options. - sess = session.Session.construct(dict( - 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() - )) - - auth_plugin = self._get_auth_plugin() - - adap = adapter.Adapter( - sess, - auth=auth_plugin, - service_type='identity', - interface='admin', - region_name=self._conf_get('region_name'), - connect_retries=self._conf_get('http_request_max_retries')) - - auth_version = self._conf_get('auth_version') - if auth_version is not None: - auth_version = discover.normalize_version_number(auth_version) - return _identity.IdentityServer( - self.log, - adap, - include_service_catalog=self._include_service_catalog, - requested_auth_version=auth_version) - - def _token_cache_factory(self): - 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'), - memcache_pool_dead_retry=self._conf_get( - 'memcache_pool_dead_retry'), - memcache_pool_maxsize=self._conf_get('memcache_pool_maxsize'), - memcache_pool_unused_timeout=self._conf_get( - 'memcache_pool_unused_timeout'), - memcache_pool_conn_get_timeout=self._conf_get( - 'memcache_pool_conn_get_timeout'), - memcache_pool_socket_timeout=self._conf_get( - 'memcache_pool_socket_timeout'), - ) - - if security_strategy: - secret_key = self._conf_get('memcache_secret_key') - return _cache.SecureTokenCache(self.log, - security_strategy, - secret_key, - **cache_kwargs) - else: - return _cache.TokenCache(self.log, **cache_kwargs) - - -def filter_factory(global_conf, **local_conf): - """Returns a WSGI filter app for use with paste.deploy.""" - conf = global_conf.copy() - conf.update(local_conf) - - def auth_filter(app): - return AuthProtocol(app, conf) - return auth_filter - - -def app_factory(global_conf, **local_conf): - conf = global_conf.copy() - conf.update(local_conf) - return AuthProtocol(None, conf) - - -# NOTE(jamielennox): Maintained here for public API compatibility. -InvalidToken = exc.InvalidToken -ServiceError = exc.ServiceError -ConfigurationError = exc.ConfigurationError -RevocationListError = exc.RevocationListError diff --git a/keystonemiddleware-moon/keystonemiddleware/auth_token/_auth.py b/keystonemiddleware-moon/keystonemiddleware/auth_token/_auth.py deleted file mode 100644 index cf7ed84d..00000000 --- a/keystonemiddleware-moon/keystonemiddleware/auth_token/_auth.py +++ /dev/null @@ -1,194 +0,0 @@ -# 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 logging - -from keystoneclient import auth -from keystoneclient.auth.identity import v2 -from keystoneclient.auth import token_endpoint -from keystoneclient import discover -from oslo_config import cfg - -from keystonemiddleware.auth_token import _base -from keystonemiddleware.i18n import _, _LW - - -_LOG = logging.getLogger(__name__) - - -class AuthTokenPlugin(auth.BaseAuthPlugin): - - def __init__(self, auth_host, auth_port, auth_protocol, auth_admin_prefix, - admin_user, admin_password, admin_tenant_name, admin_token, - identity_uri, log): - - log.warning(_LW( - "Use of the auth_admin_prefix, auth_host, auth_port, " - "auth_protocol, identity_uri, admin_token, admin_user, " - "admin_password, and admin_tenant_name configuration options is " - "deprecated in favor of auth_plugin and related options and may " - "be removed in a future release.")) - - # NOTE(jamielennox): it does appear here that our default arguments - # are backwards. We need to do it this way so that we can handle the - # same deprecation strategy for CONF and the conf variable. - if not identity_uri: - log.warning(_LW('Configuring admin URI using auth fragments. ' - 'This is deprecated, use \'identity_uri\'' - ' instead.')) - - if ':' in auth_host: - # Note(dzyu) it is an IPv6 address, so it needs to be wrapped - # with '[]' to generate a valid IPv6 URL, based on - # http://www.ietf.org/rfc/rfc2732.txt - auth_host = '[%s]' % auth_host - - identity_uri = '%s://%s:%s' % (auth_protocol, - auth_host, - auth_port) - - if auth_admin_prefix: - identity_uri = '%s/%s' % (identity_uri, - auth_admin_prefix.strip('/')) - - self._identity_uri = identity_uri.rstrip('/') - - # FIXME(jamielennox): Yes. This is wrong. We should be determining the - # plugin to use based on a combination of discovery and inputs. Much - # of this can be changed when we get keystoneclient 0.10. For now this - # hardcoded path is EXACTLY the same as the original auth_token did. - auth_url = '%s/v2.0' % self._identity_uri - - if admin_token: - log.warning(_LW( - "The admin_token option in the auth_token middleware is " - "deprecated and should not be used. The admin_user and " - "admin_password options should be used instead. The " - "admin_token option may be removed in a future release.")) - self._plugin = token_endpoint.Token(auth_url, admin_token) - else: - self._plugin = v2.Password(auth_url, - username=admin_user, - password=admin_password, - tenant_name=admin_tenant_name) - - self._LOG = log - self._discover = None - - def get_token(self, *args, **kwargs): - return self._plugin.get_token(*args, **kwargs) - - def get_endpoint(self, session, interface=None, version=None, **kwargs): - """Return an endpoint for the client. - - There are no required keyword arguments to ``get_endpoint`` as a plugin - implementation should use best effort with the information available to - determine the endpoint. - - :param session: The session object that the auth_plugin belongs to. - :type session: keystoneclient.session.Session - :param version: The version number required for this endpoint. - :type version: tuple or str - :param str interface: what visibility the endpoint should have. - - :returns: The base URL that will be used to talk to the required - service or None if not available. - :rtype: string - """ - if interface == auth.AUTH_INTERFACE: - return self._identity_uri - - if not version: - # NOTE(jamielennox): This plugin can only be used within auth_token - # and auth_token will always provide version= with requests. - return None - - if not self._discover: - self._discover = discover.Discover(session, - auth_url=self._identity_uri, - authenticated=False) - - if not self._discover.url_for(version): - # NOTE(jamielennox): The requested version is not supported by the - # identity server. - return None - - # NOTE(jamielennox): for backwards compatibility here we don't - # actually use the URL from discovery we hack it up instead. :( - # NOTE(blk-u): Normalizing the version is a workaround for bug 1450272. - # This can be removed once that's fixed. Also fix the docstring for the - # version parameter to be just "tuple". - version = discover.normalize_version_number(version) - if discover.version_match((2, 0), version): - return '%s/v2.0' % self._identity_uri - elif discover.version_match((3, 0), version): - return '%s/v3' % self._identity_uri - - # NOTE(jamielennox): This plugin will only get called from auth_token - # middleware. The middleware should never request a version that the - # plugin doesn't know how to handle. - msg = _('Invalid version asked for in auth_token plugin') - raise NotImplementedError(msg) - - def invalidate(self): - return self._plugin.invalidate() - - @classmethod - def get_options(cls): - options = super(AuthTokenPlugin, cls).get_options() - - options.extend([ - cfg.StrOpt('auth_admin_prefix', - default='', - help='Prefix to prepend at the beginning of the path. ' - 'Deprecated, use identity_uri.'), - cfg.StrOpt('auth_host', - default='127.0.0.1', - help='Host providing the admin Identity API endpoint. ' - 'Deprecated, use identity_uri.'), - cfg.IntOpt('auth_port', - default=35357, - help='Port of the admin Identity API endpoint. ' - 'Deprecated, use identity_uri.'), - cfg.StrOpt('auth_protocol', - default='https', - help='Protocol of the admin Identity API endpoint ' - '(http or https). Deprecated, use identity_uri.'), - cfg.StrOpt('identity_uri', - default=None, - help='Complete admin Identity API endpoint. This ' - 'should specify the unversioned root endpoint ' - 'e.g. https://localhost:35357/'), - cfg.StrOpt('admin_token', - secret=True, - help='This option is deprecated and may be removed in ' - 'a future release. Single shared secret with the ' - 'Keystone configuration used for bootstrapping a ' - 'Keystone installation, or otherwise bypassing ' - 'the normal authentication process. This option ' - 'should not be used, use `admin_user` and ' - '`admin_password` instead.'), - cfg.StrOpt('admin_user', - help='Service username.'), - cfg.StrOpt('admin_password', - secret=True, - help='Service user password.'), - cfg.StrOpt('admin_tenant_name', - default='admin', - help='Service tenant name.'), - ]) - - return options - - -auth.register_conf_options(cfg.CONF, _base.AUTHTOKEN_GROUP) -AuthTokenPlugin.register_conf_options(cfg.CONF, _base.AUTHTOKEN_GROUP) diff --git a/keystonemiddleware-moon/keystonemiddleware/auth_token/_base.py b/keystonemiddleware-moon/keystonemiddleware/auth_token/_base.py deleted file mode 100644 index ee4ec13c..00000000 --- a/keystonemiddleware-moon/keystonemiddleware/auth_token/_base.py +++ /dev/null @@ -1,13 +0,0 @@ -# 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. - -AUTHTOKEN_GROUP = 'keystone_authtoken' diff --git a/keystonemiddleware-moon/keystonemiddleware/auth_token/_cache.py b/keystonemiddleware-moon/keystonemiddleware/auth_token/_cache.py deleted file mode 100644 index ce5faf66..00000000 --- a/keystonemiddleware-moon/keystonemiddleware/auth_token/_cache.py +++ /dev/null @@ -1,338 +0,0 @@ -# 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 contextlib -import hashlib - -from oslo_serialization import jsonutils -import six - -from keystonemiddleware.auth_token import _exceptions as exc -from keystonemiddleware.auth_token import _memcache_crypt as memcache_crypt -from keystonemiddleware.i18n import _, _LE -from keystonemiddleware.openstack.common import memorycache - - -def _hash_key(key): - """Turn a set of arguments into a SHA256 hash. - - Using a known-length cache key is important to ensure that memcache - maximum key length is not exceeded causing failures to validate. - """ - if isinstance(key, six.text_type): - # NOTE(morganfainberg): Ensure we are always working with a bytes - # type required for the hasher. In python 2.7 it is possible to - # get a text_type (unicode). In python 3.4 all strings are - # text_type and not bytes by default. This encode coerces the - # text_type to the appropriate bytes values. - key = key.encode('utf-8') - return hashlib.sha256(key).hexdigest() - - -class _CachePool(list): - """A lazy pool of cache references.""" - - def __init__(self, cache, memcached_servers): - self._environment_cache = cache - self._memcached_servers = memcached_servers - - @contextlib.contextmanager - def reserve(self): - """Context manager to manage a pooled cache reference.""" - if self._environment_cache is not None: - # skip pooling and just use the cache from the upstream filter - yield self._environment_cache - return # otherwise the context manager will continue! - - try: - c = self.pop() - except IndexError: - # the pool is empty, so we need to create a new client - c = memorycache.get_client(self._memcached_servers) - - try: - yield c - finally: - self.append(c) - - -class _MemcacheClientPool(object): - """An advanced memcached client pool that is eventlet safe.""" - def __init__(self, memcache_servers, memcache_dead_retry=None, - memcache_pool_maxsize=None, memcache_pool_unused_timeout=None, - memcache_pool_conn_get_timeout=None, - memcache_pool_socket_timeout=None): - # NOTE(morganfainberg): import here to avoid hard dependency on - # python-memcached library. - global _memcache_pool - from keystonemiddleware.auth_token import _memcache_pool - - self._pool = _memcache_pool.MemcacheClientPool( - memcache_servers, - arguments={ - 'dead_retry': memcache_dead_retry, - 'socket_timeout': memcache_pool_socket_timeout, - }, - maxsize=memcache_pool_maxsize, - unused_timeout=memcache_pool_unused_timeout, - conn_get_timeout=memcache_pool_conn_get_timeout, - ) - - @contextlib.contextmanager - def reserve(self): - with self._pool.get() as client: - yield client - - -class TokenCache(object): - """Encapsulates the auth_token token cache functionality. - - auth_token caches tokens that it's seen so that when a token is re-used the - middleware doesn't have to do a more expensive operation (like going to the - identity server) to validate the token. - - initialize() must be called before calling the other methods. - - Store a valid token in the cache using store(); mark a token as invalid in - the cache using store_invalid(). - - Check if a token is in the cache and retrieve it using get(). - - """ - - _CACHE_KEY_TEMPLATE = 'tokens/%s' - _INVALID_INDICATOR = 'invalid' - - def __init__(self, log, cache_time=None, - env_cache_name=None, memcached_servers=None, - use_advanced_pool=False, memcache_pool_dead_retry=None, - memcache_pool_maxsize=None, memcache_pool_unused_timeout=None, - memcache_pool_conn_get_timeout=None, - memcache_pool_socket_timeout=None): - self._LOG = log - self._cache_time = cache_time - self._env_cache_name = env_cache_name - self._memcached_servers = memcached_servers - self._use_advanced_pool = use_advanced_pool - self._memcache_pool_dead_retry = memcache_pool_dead_retry, - self._memcache_pool_maxsize = memcache_pool_maxsize, - self._memcache_pool_unused_timeout = memcache_pool_unused_timeout - self._memcache_pool_conn_get_timeout = memcache_pool_conn_get_timeout - self._memcache_pool_socket_timeout = memcache_pool_socket_timeout - - self._cache_pool = None - self._initialized = False - - def _get_cache_pool(self, cache, memcache_servers, use_advanced_pool=False, - memcache_dead_retry=None, memcache_pool_maxsize=None, - memcache_pool_unused_timeout=None, - memcache_pool_conn_get_timeout=None, - memcache_pool_socket_timeout=None): - if use_advanced_pool is True and memcache_servers and cache is None: - return _MemcacheClientPool( - memcache_servers, - memcache_dead_retry=memcache_dead_retry, - memcache_pool_maxsize=memcache_pool_maxsize, - memcache_pool_unused_timeout=memcache_pool_unused_timeout, - memcache_pool_conn_get_timeout=memcache_pool_conn_get_timeout, - memcache_pool_socket_timeout=memcache_pool_socket_timeout) - else: - return _CachePool(cache, memcache_servers) - - def initialize(self, env): - if self._initialized: - return - - self._cache_pool = self._get_cache_pool( - env.get(self._env_cache_name), - self._memcached_servers, - use_advanced_pool=self._use_advanced_pool, - memcache_dead_retry=self._memcache_pool_dead_retry, - memcache_pool_maxsize=self._memcache_pool_maxsize, - memcache_pool_unused_timeout=self._memcache_pool_unused_timeout, - memcache_pool_conn_get_timeout=self._memcache_pool_conn_get_timeout - ) - - self._initialized = True - - def store(self, token_id, data): - """Put token data into the cache. - """ - self._LOG.debug('Storing token in cache') - self._cache_store(token_id, data) - - def store_invalid(self, token_id): - """Store invalid token in cache.""" - self._LOG.debug('Marking token as unauthorized in cache') - self._cache_store(token_id, self._INVALID_INDICATOR) - - def _get_cache_key(self, token_id): - """Get a unique key for this token id. - - Turn the token_id into something that can uniquely identify that token - in a key value store. - - As this is generally the first function called in a key lookup this - function also returns a context object. This context object is not - modified or used by the Cache object but is passed back on subsequent - functions so that decryption or other data can be shared throughout a - cache lookup. - - :param str token_id: The unique token id. - - :returns: A tuple of a string key and an implementation specific - context object - """ - # NOTE(jamielennox): in the basic implementation there is no need for - # a context so just pass None as it will only get passed back later. - unused_context = None - return self._CACHE_KEY_TEMPLATE % _hash_key(token_id), unused_context - - def _deserialize(self, data, context): - """Deserialize data from the cache back into python objects. - - Take data retrieved from the cache and return an appropriate python - dictionary. - - :param str data: The data retrieved from the cache. - :param object context: The context that was returned from - _get_cache_key. - - :returns: The python object that was saved. - """ - # memory cache will handle deserialization for us - return data - - def _serialize(self, data, context): - """Serialize data so that it can be saved to the cache. - - Take python objects and serialize them so that they can be saved into - the cache. - - :param object data: The data to be cached. - :param object context: The context that was returned from - _get_cache_key. - - :returns: The python object that was saved. - """ - # memory cache will handle serialization for us - return data - - def get(self, token_id): - """Return token information from cache. - - If token is invalid raise exc.InvalidToken - return token only if fresh (not expired). - """ - - if not token_id: - # Nothing to do - return - - key, context = self._get_cache_key(token_id) - - with self._cache_pool.reserve() as cache: - serialized = cache.get(key) - - if serialized is None: - return None - - if isinstance(serialized, six.text_type): - serialized = serialized.encode('utf8') - data = self._deserialize(serialized, context) - - # Note that _INVALID_INDICATOR and (data, expires) are the only - # valid types of serialized cache entries, so there is not - # a collision with jsonutils.loads(serialized) == None. - if not isinstance(data, six.string_types): - data = data.decode('utf-8') - cached = jsonutils.loads(data) - if cached == self._INVALID_INDICATOR: - self._LOG.debug('Cached Token is marked unauthorized') - raise exc.InvalidToken(_('Token authorization failed')) - - # NOTE(jamielennox): Cached values used to be stored as a tuple of data - # and expiry time. They no longer are but we have to allow some time to - # transition the old format so if it's a tuple just return the data. - try: - data, expires = cached - except ValueError: - data = cached - - return data - - def _cache_store(self, token_id, data): - """Store value into memcache. - - data may be _INVALID_INDICATOR or a tuple like (data, expires) - - """ - data = jsonutils.dumps(data) - if isinstance(data, six.text_type): - data = data.encode('utf-8') - - cache_key, context = self._get_cache_key(token_id) - data_to_store = self._serialize(data, context) - - with self._cache_pool.reserve() as cache: - cache.set(cache_key, data_to_store, time=self._cache_time) - - -class SecureTokenCache(TokenCache): - """A token cache that stores tokens encrypted. - - A more secure version of TokenCache that will encrypt tokens before - caching them. - """ - - def __init__(self, log, security_strategy, secret_key, **kwargs): - super(SecureTokenCache, self).__init__(log, **kwargs) - - security_strategy = security_strategy.upper() - - if security_strategy not in ('MAC', 'ENCRYPT'): - msg = _('memcache_security_strategy must be ENCRYPT or MAC') - raise exc.ConfigurationError(msg) - if not secret_key: - msg = _('memcache_secret_key must be defined when a ' - 'memcache_security_strategy is defined') - raise exc.ConfigurationError(msg) - - if isinstance(security_strategy, six.string_types): - security_strategy = security_strategy.encode('utf-8') - if isinstance(secret_key, six.string_types): - secret_key = secret_key.encode('utf-8') - - self._security_strategy = security_strategy - self._secret_key = secret_key - - def _get_cache_key(self, token_id): - context = memcache_crypt.derive_keys(token_id, - self._secret_key, - self._security_strategy) - key = self._CACHE_KEY_TEMPLATE % memcache_crypt.get_cache_key(context) - return key, context - - def _deserialize(self, data, context): - try: - # unprotect_data will return None if raw_cached is None - return memcache_crypt.unprotect_data(context, data) - except Exception: - msg = _LE('Failed to decrypt/verify cache data') - self._LOG.exception(msg) - - # this should have the same effect as data not - # found in cache - return None - - def _serialize(self, data, context): - return memcache_crypt.protect_data(context, data) diff --git a/keystonemiddleware-moon/keystonemiddleware/auth_token/_exceptions.py b/keystonemiddleware-moon/keystonemiddleware/auth_token/_exceptions.py deleted file mode 100644 index be045c96..00000000 --- a/keystonemiddleware-moon/keystonemiddleware/auth_token/_exceptions.py +++ /dev/null @@ -1,27 +0,0 @@ -# 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 InvalidToken(Exception): - pass - - -class ServiceError(Exception): - pass - - -class ConfigurationError(Exception): - pass - - -class RevocationListError(Exception): - pass diff --git a/keystonemiddleware-moon/keystonemiddleware/auth_token/_identity.py b/keystonemiddleware-moon/keystonemiddleware/auth_token/_identity.py deleted file mode 100644 index 6fbeac27..00000000 --- a/keystonemiddleware-moon/keystonemiddleware/auth_token/_identity.py +++ /dev/null @@ -1,252 +0,0 @@ -# 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 functools - -from keystoneclient import auth -from keystoneclient import discover -from keystoneclient import exceptions -from keystoneclient.v2_0 import client as v2_client -from keystoneclient.v3 import client as v3_client -from six.moves import urllib - -from keystonemiddleware.auth_token import _auth -from keystonemiddleware.auth_token import _exceptions as exc -from keystonemiddleware.i18n import _, _LE, _LI, _LW - - -def _convert_fetch_cert_exception(fetch_cert): - @functools.wraps(fetch_cert) - def wrapper(self): - try: - text = fetch_cert(self) - except exceptions.HTTPError as e: - raise exceptions.CertificateConfigError(e.details) - return text - - return wrapper - - -class _RequestStrategy(object): - - AUTH_VERSION = None - - def __init__(self, adap, include_service_catalog=None): - self._include_service_catalog = include_service_catalog - - def verify_token(self, user_token): - pass - - @_convert_fetch_cert_exception - def fetch_signing_cert(self): - return self._fetch_signing_cert() - - def _fetch_signing_cert(self): - pass - - @_convert_fetch_cert_exception - def fetch_ca_cert(self): - return self._fetch_ca_cert() - - def _fetch_ca_cert(self): - pass - - def fetch_revocation_list(self): - pass - - -class _V2RequestStrategy(_RequestStrategy): - - AUTH_VERSION = (2, 0) - - def __init__(self, adap, **kwargs): - super(_V2RequestStrategy, self).__init__(adap, **kwargs) - self._client = v2_client.Client(session=adap) - - def verify_token(self, token): - auth_ref = self._client.tokens.validate_access_info(token) - - if not auth_ref: - msg = _('Failed to fetch token data from identity server') - raise exc.InvalidToken(msg) - - return {'access': auth_ref} - - def _fetch_signing_cert(self): - return self._client.certificates.get_signing_certificate() - - def _fetch_ca_cert(self): - return self._client.certificates.get_ca_certificate() - - def fetch_revocation_list(self): - return self._client.tokens.get_revoked() - - -class _V3RequestStrategy(_RequestStrategy): - - AUTH_VERSION = (3, 0) - - def __init__(self, adap, **kwargs): - super(_V3RequestStrategy, self).__init__(adap, **kwargs) - self._client = v3_client.Client(session=adap) - - def verify_token(self, token): - auth_ref = self._client.tokens.validate( - token, - include_catalog=self._include_service_catalog) - - if not auth_ref: - msg = _('Failed to fetch token data from identity server') - raise exc.InvalidToken(msg) - - return {'token': auth_ref} - - def _fetch_signing_cert(self): - return self._client.simple_cert.get_certificates() - - def _fetch_ca_cert(self): - return self._client.simple_cert.get_ca_certificates() - - def fetch_revocation_list(self): - return self._client.tokens.get_revoked() - - -_REQUEST_STRATEGIES = [_V3RequestStrategy, _V2RequestStrategy] - - -class IdentityServer(object): - """Base class for operations on the Identity API server. - - The auth_token middleware needs to communicate with the Identity API server - to validate UUID tokens, fetch the revocation list, signing certificates, - etc. This class encapsulates the data and methods to perform these - operations. - - """ - - def __init__(self, log, adap, include_service_catalog=None, - requested_auth_version=None): - self._LOG = log - self._adapter = adap - self._include_service_catalog = include_service_catalog - self._requested_auth_version = requested_auth_version - - # Built on-demand with self._request_strategy. - self._request_strategy_obj = None - - @property - def auth_uri(self): - auth_uri = self._adapter.get_endpoint(interface=auth.AUTH_INTERFACE) - - # NOTE(jamielennox): This weird stripping of the prefix hack is - # only relevant to the legacy case. We urljoin '/' to get just the - # base URI as this is the original behaviour. - if isinstance(self._adapter.auth, _auth.AuthTokenPlugin): - auth_uri = urllib.parse.urljoin(auth_uri, '/').rstrip('/') - - return auth_uri - - @property - def auth_version(self): - return self._request_strategy.AUTH_VERSION - - @property - def _request_strategy(self): - if not self._request_strategy_obj: - strategy_class = self._get_strategy_class() - self._adapter.version = strategy_class.AUTH_VERSION - - self._request_strategy_obj = strategy_class( - self._adapter, - include_service_catalog=self._include_service_catalog) - - return self._request_strategy_obj - - def _get_strategy_class(self): - if self._requested_auth_version: - # A specific version was requested. - if discover.version_match(_V3RequestStrategy.AUTH_VERSION, - self._requested_auth_version): - return _V3RequestStrategy - - # The version isn't v3 so we don't know what to do. Just assume V2. - return _V2RequestStrategy - - # Specific version was not requested then we fall through to - # discovering available versions from the server - for klass in _REQUEST_STRATEGIES: - if self._adapter.get_endpoint(version=klass.AUTH_VERSION): - msg = _LI('Auth Token confirmed use of %s apis') - self._LOG.info(msg, self._requested_auth_version) - return klass - - versions = ['v%d.%d' % s.AUTH_VERSION for s in _REQUEST_STRATEGIES] - self._LOG.error(_LE('No attempted versions [%s] supported by server'), - ', '.join(versions)) - - msg = _('No compatible apis supported by server') - raise exc.ServiceError(msg) - - def verify_token(self, user_token, retry=True): - """Authenticate user token with identity server. - - :param user_token: user's token id - :param retry: flag that forces the middleware to retry - user authentication when an indeterminate - response is received. Optional. - :returns: access info received from identity server on success - :rtype: :py:class:`keystoneclient.access.AccessInfo` - :raises exc.InvalidToken: if token is rejected - :raises exc.ServiceError: if unable to authenticate token - - """ - try: - auth_ref = self._request_strategy.verify_token(user_token) - except exceptions.NotFound as e: - self._LOG.warning(_LW('Authorization failed for token')) - self._LOG.warning(_LW('Identity response: %s'), e.response.text) - raise exc.InvalidToken(_('Token authorization failed')) - except exceptions.Unauthorized as e: - self._LOG.info(_LI('Identity server rejected authorization')) - self._LOG.warning(_LW('Identity response: %s'), e.response.text) - if retry: - self._LOG.info(_LI('Retrying validation')) - return self.verify_token(user_token, False) - msg = _('Identity server rejected authorization necessary to ' - 'fetch token data') - raise exc.ServiceError(msg) - except exceptions.HttpError as e: - self._LOG.error( - _LE('Bad response code while validating token: %s'), - e.http_status) - self._LOG.warning(_LW('Identity response: %s'), e.response.text) - msg = _('Failed to fetch token data from identity server') - raise exc.ServiceError(msg) - else: - return auth_ref - - def fetch_revocation_list(self): - try: - data = self._request_strategy.fetch_revocation_list() - except exceptions.HTTPError as e: - msg = _('Failed to fetch token revocation list: %d') - raise exc.RevocationListError(msg % e.http_status) - if 'signed' not in data: - msg = _('Revocation list improperly formatted.') - raise exc.RevocationListError(msg) - return data['signed'] - - def fetch_signing_cert(self): - return self._request_strategy.fetch_signing_cert() - - def fetch_ca_cert(self): - return self._request_strategy.fetch_ca_cert() diff --git a/keystonemiddleware-moon/keystonemiddleware/auth_token/_memcache_crypt.py b/keystonemiddleware-moon/keystonemiddleware/auth_token/_memcache_crypt.py deleted file mode 100644 index 2e45571f..00000000 --- a/keystonemiddleware-moon/keystonemiddleware/auth_token/_memcache_crypt.py +++ /dev/null @@ -1,210 +0,0 @@ -# Copyright 2010-2013 OpenStack Foundation -# -# 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. - -""" -Utilities for memcache encryption and integrity check. - -Data should be serialized before entering these functions. Encryption -has a dependency on the pycrypto. If pycrypto is not available, -CryptoUnavailableError will be raised. - -This module will not be called unless signing or encryption is enabled -in the config. It will always validate signatures, and will decrypt -data if encryption is enabled. It is not valid to mix protection -modes. - -""" - -import base64 -import functools -import hashlib -import hmac -import math -import os -import six -import sys - -from keystonemiddleware.i18n import _ - -# make sure pycrypto is available -try: - from Crypto.Cipher import AES -except ImportError: - AES = None - -HASH_FUNCTION = hashlib.sha384 -DIGEST_LENGTH = HASH_FUNCTION().digest_size -DIGEST_SPLIT = DIGEST_LENGTH // 3 -DIGEST_LENGTH_B64 = 4 * int(math.ceil(DIGEST_LENGTH / 3.0)) - - -class InvalidMacError(Exception): - """raise when unable to verify MACed data. - - This usually indicates that data had been expectedly modified in memcache. - - """ - pass - - -class DecryptError(Exception): - """raise when unable to decrypt encrypted data. - - """ - pass - - -class CryptoUnavailableError(Exception): - """raise when Python Crypto module is not available. - - """ - pass - - -def assert_crypto_availability(f): - """Ensure Crypto module is available.""" - - @functools.wraps(f) - def wrapper(*args, **kwds): - if AES is None: - raise CryptoUnavailableError() - return f(*args, **kwds) - return wrapper - - -if sys.version_info >= (3, 3): - constant_time_compare = hmac.compare_digest -else: - def constant_time_compare(first, second): - """Returns True if both string inputs are equal, otherwise False. - - This function should take a constant amount of time regardless of - how many characters in the strings match. - - """ - if len(first) != len(second): - return False - result = 0 - if six.PY3 and isinstance(first, bytes) and isinstance(second, bytes): - for x, y in zip(first, second): - result |= x ^ y - else: - for x, y in zip(first, second): - result |= ord(x) ^ ord(y) - return result == 0 - - -def derive_keys(token, secret, strategy): - """Derives keys for MAC and ENCRYPTION from the user-provided - secret. The resulting keys should be passed to the protect and - unprotect functions. - - As suggested by NIST Special Publication 800-108, this uses the - first 128 bits from the sha384 KDF for the obscured cache key - value, the second 128 bits for the message authentication key and - the remaining 128 bits for the encryption key. - - This approach is faster than computing a separate hmac as the KDF - for each desired key. - """ - digest = hmac.new(secret, token + strategy, HASH_FUNCTION).digest() - return {'CACHE_KEY': digest[:DIGEST_SPLIT], - 'MAC': digest[DIGEST_SPLIT: 2 * DIGEST_SPLIT], - 'ENCRYPTION': digest[2 * DIGEST_SPLIT:], - 'strategy': strategy} - - -def sign_data(key, data): - """Sign the data using the defined function and the derived key.""" - mac = hmac.new(key, data, HASH_FUNCTION).digest() - return base64.b64encode(mac) - - -@assert_crypto_availability -def encrypt_data(key, data): - """Encrypt the data with the given secret key. - - Padding is n bytes of the value n, where 1 <= n <= blocksize. - """ - iv = os.urandom(16) - cipher = AES.new(key, AES.MODE_CBC, iv) - padding = 16 - len(data) % 16 - return iv + cipher.encrypt(data + six.int2byte(padding) * padding) - - -@assert_crypto_availability -def decrypt_data(key, data): - """Decrypt the data with the given secret key.""" - iv = data[:16] - cipher = AES.new(key, AES.MODE_CBC, iv) - try: - result = cipher.decrypt(data[16:]) - except Exception: - raise DecryptError(_('Encrypted data appears to be corrupted.')) - - # Strip the last n padding bytes where n is the last value in - # the plaintext - return result[:-1 * six.byte2int([result[-1]])] - - -def protect_data(keys, data): - """Given keys and serialized data, returns an appropriately - protected string suitable for storage in the cache. - - """ - if keys['strategy'] == b'ENCRYPT': - data = encrypt_data(keys['ENCRYPTION'], data) - - encoded_data = base64.b64encode(data) - - signature = sign_data(keys['MAC'], encoded_data) - return signature + encoded_data - - -def unprotect_data(keys, signed_data): - """Given keys and cached string data, verifies the signature, - decrypts if necessary, and returns the original serialized data. - - """ - # cache backends return None when no data is found. We don't mind - # that this particular special value is unsigned. - if signed_data is None: - return None - - # First we calculate the signature - provided_mac = signed_data[:DIGEST_LENGTH_B64] - calculated_mac = sign_data( - keys['MAC'], - signed_data[DIGEST_LENGTH_B64:]) - - # Then verify that it matches the provided value - if not constant_time_compare(provided_mac, calculated_mac): - raise InvalidMacError(_('Invalid MAC; data appears to be corrupted.')) - - data = base64.b64decode(signed_data[DIGEST_LENGTH_B64:]) - - # then if necessary decrypt the data - if keys['strategy'] == b'ENCRYPT': - data = decrypt_data(keys['ENCRYPTION'], data) - - return data - - -def get_cache_key(keys): - """Given keys generated by derive_keys(), returns a base64 - encoded value suitable for use as a cache key in memcached. - - """ - return base64.b64encode(keys['CACHE_KEY']) diff --git a/keystonemiddleware-moon/keystonemiddleware/auth_token/_memcache_pool.py b/keystonemiddleware-moon/keystonemiddleware/auth_token/_memcache_pool.py deleted file mode 100644 index 77652868..00000000 --- a/keystonemiddleware-moon/keystonemiddleware/auth_token/_memcache_pool.py +++ /dev/null @@ -1,184 +0,0 @@ -# Copyright 2014 Mirantis Inc -# 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. - -"""Thread-safe connection pool for python-memcached.""" - -# NOTE(yorik-sar): this file is copied between keystone and keystonemiddleware -# and should be kept in sync until we can use external library for this. - -import collections -import contextlib -import itertools -import logging -import time - -from six.moves import queue - -from keystonemiddleware.i18n import _LC - - -_PoolItem = collections.namedtuple('_PoolItem', ['ttl', 'connection']) - - -class ConnectionGetTimeoutException(Exception): - pass - - -class ConnectionPool(queue.Queue): - """Base connection pool class - - This class implements the basic connection pool logic as an abstract base - class. - """ - def __init__(self, maxsize, unused_timeout, conn_get_timeout=None): - """Initialize the connection pool. - - :param maxsize: maximum number of client connections for the pool - :type maxsize: int - :param unused_timeout: idle time to live for unused clients (in - seconds). If a client connection object has been - in the pool and idle for longer than the - unused_timeout, it will be reaped. This is to - ensure resources are released as utilization - goes down. - :type unused_timeout: int - :param conn_get_timeout: maximum time in seconds to wait for a - connection. If set to `None` timeout is - indefinite. - :type conn_get_timeout: int - """ - queue.Queue.__init__(self, maxsize) - self._unused_timeout = unused_timeout - self._connection_get_timeout = conn_get_timeout - self._acquired = 0 - self._LOG = logging.getLogger(__name__) - - def _create_connection(self): - raise NotImplementedError - - def _destroy_connection(self, conn): - raise NotImplementedError - - @contextlib.contextmanager - def acquire(self): - try: - conn = self.get(timeout=self._connection_get_timeout) - except queue.Empty: - self._LOG.critical(_LC('Unable to get a connection from pool id ' - '%(id)s after %(seconds)s seconds.'), - {'id': id(self), - 'seconds': self._connection_get_timeout}) - raise ConnectionGetTimeoutException() - try: - yield conn - finally: - self.put(conn) - - def _qsize(self): - return self.maxsize - self._acquired - - if not hasattr(queue.Queue, '_qsize'): - qsize = _qsize - - def _get(self): - if self.queue: - conn = self.queue.pop().connection - else: - conn = self._create_connection() - self._acquired += 1 - return conn - - def _put(self, conn): - self.queue.append(_PoolItem( - ttl=time.time() + self._unused_timeout, - connection=conn, - )) - self._acquired -= 1 - # Drop all expired connections from the right end of the queue - now = time.time() - while self.queue and self.queue[0].ttl < now: - conn = self.queue.popleft().connection - self._destroy_connection(conn) - - -class MemcacheClientPool(ConnectionPool): - def __init__(self, urls, arguments, **kwargs): - ConnectionPool.__init__(self, **kwargs) - self._urls = urls - self._arguments = arguments - # NOTE(morganfainberg): The host objects expect an int for the - # deaduntil value. Initialize this at 0 for each host with 0 indicating - # the host is not dead. - self._hosts_deaduntil = [0] * len(urls) - - # NOTE(morganfainberg): Lazy import to allow middleware to work with - # python 3k even if memcache will not due to python 3k - # incompatibilities within the python-memcache library. - global memcache - import memcache - - # This 'class' is taken from http://stackoverflow.com/a/22520633/238308 - # Don't inherit client from threading.local so that we can reuse - # clients in different threads - MemcacheClient = type('_MemcacheClient', (object,), - dict(memcache.Client.__dict__)) - - self._memcache_client_class = MemcacheClient - - def _create_connection(self): - return self._memcache_client_class(self._urls, **self._arguments) - - def _destroy_connection(self, conn): - conn.disconnect_all() - - def _get(self): - conn = ConnectionPool._get(self) - try: - # Propagate host state known to us to this client's list - now = time.time() - for deaduntil, host in zip(self._hosts_deaduntil, conn.servers): - if deaduntil > now and host.deaduntil <= now: - host.mark_dead('propagating death mark from the pool') - host.deaduntil = deaduntil - except Exception: - # We need to be sure that connection doesn't leak from the pool. - # This code runs before we enter context manager's try-finally - # block, so we need to explicitly release it here - ConnectionPool._put(self, conn) - raise - return conn - - def _put(self, conn): - try: - # If this client found that one of the hosts is dead, mark it as - # such in our internal list - now = time.time() - for i, deaduntil, host in zip(itertools.count(), - self._hosts_deaduntil, - conn.servers): - # Do nothing if we already know this host is dead - if deaduntil <= now: - if host.deaduntil > now: - self._hosts_deaduntil[i] = host.deaduntil - else: - self._hosts_deaduntil[i] = 0 - # If all hosts are dead we should forget that they're dead. This - # way we won't get completely shut off until dead_retry seconds - # pass, but will be checking servers as frequent as we can (over - # way smaller socket_timeout) - if all(deaduntil > now for deaduntil in self._hosts_deaduntil): - self._hosts_deaduntil[:] = [0] * len(self._hosts_deaduntil) - finally: - ConnectionPool._put(self, conn) diff --git a/keystonemiddleware-moon/keystonemiddleware/auth_token/_request.py b/keystonemiddleware-moon/keystonemiddleware/auth_token/_request.py deleted file mode 100644 index 72fd5380..00000000 --- a/keystonemiddleware-moon/keystonemiddleware/auth_token/_request.py +++ /dev/null @@ -1,224 +0,0 @@ -# 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 itertools - -from oslo_serialization import jsonutils -import six -import webob - - -def _v3_to_v2_catalog(catalog): - """Convert a catalog to v2 format. - - X_SERVICE_CATALOG must be specified in v2 format. If you get a token - that is in v3 convert it. - """ - v2_services = [] - for v3_service in catalog: - # first copy over the entries we allow for the service - v2_service = {'type': v3_service['type']} - try: - v2_service['name'] = v3_service['name'] - except KeyError: - pass - - # now convert the endpoints. Because in v3 we specify region per - # URL not per group we have to collect all the entries of the same - # region together before adding it to the new service. - regions = {} - for v3_endpoint in v3_service.get('endpoints', []): - region_name = v3_endpoint.get('region') - try: - region = regions[region_name] - except KeyError: - region = {'region': region_name} if region_name else {} - regions[region_name] = region - - interface_name = v3_endpoint['interface'].lower() + 'URL' - region[interface_name] = v3_endpoint['url'] - - v2_service['endpoints'] = list(regions.values()) - v2_services.append(v2_service) - - return v2_services - - -# NOTE(jamielennox): this should probably be moved into its own file, but at -# the moment there's no real logic here so just keep it locally. -class _AuthTokenResponse(webob.Response): - - default_content_type = None # prevents webob assigning a content type - - -class _AuthTokenRequest(webob.Request): - - ResponseClass = _AuthTokenResponse - - _HEADER_TEMPLATE = { - 'X%s-Domain-Id': 'domain_id', - 'X%s-Domain-Name': 'domain_name', - 'X%s-Project-Id': 'project_id', - 'X%s-Project-Name': 'project_name', - 'X%s-Project-Domain-Id': 'project_domain_id', - 'X%s-Project-Domain-Name': 'project_domain_name', - 'X%s-User-Id': 'user_id', - 'X%s-User-Name': 'username', - 'X%s-User-Domain-Id': 'user_domain_id', - 'X%s-User-Domain-Name': 'user_domain_name', - } - - _ROLES_TEMPLATE = 'X%s-Roles' - - _USER_HEADER_PREFIX = '' - _SERVICE_HEADER_PREFIX = '-Service' - - _USER_STATUS_HEADER = 'X-Identity-Status' - _SERVICE_STATUS_HEADER = 'X-Service-Identity-Status' - - _SERVICE_CATALOG_HEADER = 'X-Service-Catalog' - _TOKEN_AUTH = 'keystone.token_auth' - - _CONFIRMED = 'Confirmed' - _INVALID = 'Invalid' - - # header names that have been deprecated in favour of something else. - _DEPRECATED_HEADER_MAP = { - 'X-Role': 'X-Roles', - 'X-User': 'X-User-Name', - 'X-Tenant-Id': 'X-Project-Id', - 'X-Tenant-Name': 'X-Project-Name', - 'X-Tenant': 'X-Project-Name', - } - - def _confirmed(cls, value): - return cls._CONFIRMED if value else cls._INVALID - - @property - def user_token_valid(self): - """User token is marked as valid. - - :returns: True if the X-Identity-Status header is set to Confirmed. - :rtype: bool - """ - return self.headers[self._USER_STATUS_HEADER] == self._CONFIRMED - - @user_token_valid.setter - def user_token_valid(self, value): - self.headers[self._USER_STATUS_HEADER] = self._confirmed(value) - - @property - def user_token(self): - return self.headers.get('X-Auth-Token', - self.headers.get('X-Storage-Token')) - - @property - def service_token_valid(self): - """Service token is marked as valid. - - :returns: True if the X-Service-Identity-Status header - is set to Confirmed. - :rtype: bool - """ - return self.headers[self._SERVICE_STATUS_HEADER] == self._CONFIRMED - - @service_token_valid.setter - def service_token_valid(self, value): - self.headers[self._SERVICE_STATUS_HEADER] = self._confirmed(value) - - @property - def service_token(self): - return self.headers.get('X-Service-Token') - - def _set_auth_headers(self, auth_ref, prefix): - names = ','.join(auth_ref.role_names) - self.headers[self._ROLES_TEMPLATE % prefix] = names - - for header_tmplt, attr in six.iteritems(self._HEADER_TEMPLATE): - self.headers[header_tmplt % prefix] = getattr(auth_ref, attr) - - def set_user_headers(self, auth_ref, include_service_catalog): - """Convert token object into headers. - - Build headers that represent authenticated user - see main - doc info at start of __init__ file for details of headers to be defined - """ - self._set_auth_headers(auth_ref, self._USER_HEADER_PREFIX) - - for k, v in six.iteritems(self._DEPRECATED_HEADER_MAP): - self.headers[k] = self.headers[v] - - if include_service_catalog and auth_ref.has_service_catalog(): - catalog = auth_ref.service_catalog.get_data() - if auth_ref.version == 'v3': - catalog = _v3_to_v2_catalog(catalog) - - c = jsonutils.dumps(catalog) - self.headers[self._SERVICE_CATALOG_HEADER] = c - - self.user_token_valid = True - - def set_service_headers(self, auth_ref): - """Convert token object into service headers. - - Build headers that represent authenticated user - see main - doc info at start of __init__ file for details of headers to be defined - """ - self._set_auth_headers(auth_ref, self._SERVICE_HEADER_PREFIX) - self.service_token_valid = True - - def _all_auth_headers(self): - """All the authentication headers that can be set on the request""" - yield self._SERVICE_CATALOG_HEADER - yield self._USER_STATUS_HEADER - yield self._SERVICE_STATUS_HEADER - - for header in self._DEPRECATED_HEADER_MAP: - yield header - - prefixes = (self._USER_HEADER_PREFIX, self._SERVICE_HEADER_PREFIX) - - for tmpl, prefix in itertools.product(self._HEADER_TEMPLATE, prefixes): - yield tmpl % prefix - - for prefix in prefixes: - yield self._ROLES_TEMPLATE % prefix - - def remove_auth_headers(self): - """Remove headers so a user can't fake authentication.""" - for header in self._all_auth_headers(): - self.headers.pop(header, None) - - @property - def auth_type(self): - """The authentication type that was performed by the web server. - - The returned string value is always lower case. - - :returns: The AUTH_TYPE environ string or None if not present. - :rtype: str or None - """ - try: - auth_type = self.environ['AUTH_TYPE'] - except KeyError: - return None - else: - return auth_type.lower() - - @property - def token_auth(self): - """The auth plugin that will be associated with this request""" - return self.environ.get(self._TOKEN_AUTH) - - @token_auth.setter - def token_auth(self, v): - self.environ[self._TOKEN_AUTH] = v diff --git a/keystonemiddleware-moon/keystonemiddleware/auth_token/_revocations.py b/keystonemiddleware-moon/keystonemiddleware/auth_token/_revocations.py deleted file mode 100644 index a68356a8..00000000 --- a/keystonemiddleware-moon/keystonemiddleware/auth_token/_revocations.py +++ /dev/null @@ -1,128 +0,0 @@ -# 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 datetime -import logging -import os - -from oslo_serialization import jsonutils -from oslo_utils import timeutils - -from keystonemiddleware.auth_token import _exceptions as exc -from keystonemiddleware.i18n import _ - -_LOG = logging.getLogger(__name__) - - -class Revocations(object): - _FILE_NAME = 'revoked.pem' - - def __init__(self, timeout, signing_directory, identity_server, - cms_verify, log=_LOG): - self._cache_timeout = timeout - self._signing_directory = signing_directory - self._identity_server = identity_server - self._cms_verify = cms_verify - self._log = log - - self._fetched_time_prop = None - self._list_prop = None - - @property - def _fetched_time(self): - if not self._fetched_time_prop: - # If the fetched list has been written to disk, use its - # modification time. - file_path = self._signing_directory.calc_path(self._FILE_NAME) - if os.path.exists(file_path): - mtime = os.path.getmtime(file_path) - fetched_time = datetime.datetime.utcfromtimestamp(mtime) - # Otherwise the list will need to be fetched. - else: - fetched_time = datetime.datetime.min - self._fetched_time_prop = fetched_time - return self._fetched_time_prop - - @_fetched_time.setter - def _fetched_time(self, value): - self._fetched_time_prop = value - - def _fetch(self): - revocation_list_data = self._identity_server.fetch_revocation_list() - return self._cms_verify(revocation_list_data) - - @property - def _list(self): - timeout = self._fetched_time + self._cache_timeout - list_is_current = timeutils.utcnow() < timeout - - if list_is_current: - # Load the list from disk if required - if not self._list_prop: - self._list_prop = jsonutils.loads( - self._signing_directory.read_file(self._FILE_NAME)) - else: - self._list = self._fetch() - return self._list_prop - - @_list.setter - def _list(self, value): - """Save a revocation list to memory and to disk. - - :param value: A json-encoded revocation list - - """ - self._list_prop = jsonutils.loads(value) - self._fetched_time = timeutils.utcnow() - self._signing_directory.write_file(self._FILE_NAME, value) - - def _is_revoked(self, token_id): - """Indicate whether the token_id appears in the revocation list.""" - revoked_tokens = self._list.get('revoked', None) - if not revoked_tokens: - return False - - revoked_ids = (x['id'] for x in revoked_tokens) - return token_id in revoked_ids - - def _any_revoked(self, token_ids): - for token_id in token_ids: - if self._is_revoked(token_id): - return True - return False - - def check(self, token_ids): - if self._any_revoked(token_ids): - self._log.debug('Token is marked as having been revoked') - raise exc.InvalidToken(_('Token has been revoked')) - - def check_by_audit_id(self, audit_ids): - """Check whether the audit_id appears in the revocation list. - - :raises keystonemiddleware.auth_token._exceptions.InvalidToken: - if the audit ID(s) appear in the revocation list. - - """ - revoked_tokens = self._list.get('revoked', None) - if not revoked_tokens: - # There's no revoked tokens, so nothing to do. - return - - # The audit_id may not be present in the revocation events because - # earlier versions of the identity server didn't provide them. - revoked_ids = set( - x['audit_id'] for x in revoked_tokens if 'audit_id' in x) - for audit_id in audit_ids: - if audit_id in revoked_ids: - self._log.debug( - 'Token is marked as having been revoked by audit id') - raise exc.InvalidToken(_('Token has been revoked')) diff --git a/keystonemiddleware-moon/keystonemiddleware/auth_token/_signing_dir.py b/keystonemiddleware-moon/keystonemiddleware/auth_token/_signing_dir.py deleted file mode 100644 index f8b1a410..00000000 --- a/keystonemiddleware-moon/keystonemiddleware/auth_token/_signing_dir.py +++ /dev/null @@ -1,83 +0,0 @@ -# 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 logging -import os -import stat -import tempfile - -import six - -from keystonemiddleware.auth_token import _exceptions as exc -from keystonemiddleware.i18n import _, _LI, _LW - -_LOG = logging.getLogger(__name__) - - -class SigningDirectory(object): - - def __init__(self, directory_name=None, log=None): - self._log = log or _LOG - - if directory_name is None: - directory_name = tempfile.mkdtemp(prefix='keystone-signing-') - self._log.info( - _LI('Using %s as cache directory for signing certificate'), - directory_name) - self._directory_name = directory_name - - self._verify_signing_dir() - - def write_file(self, file_name, new_contents): - - # In Python2, encoding is slow so the following check avoids it if it - # is not absolutely necessary. - if isinstance(new_contents, six.text_type): - new_contents = new_contents.encode('utf-8') - - def _atomic_write(): - with tempfile.NamedTemporaryFile(dir=self._directory_name, - delete=False) as f: - f.write(new_contents) - os.rename(f.name, self.calc_path(file_name)) - - try: - _atomic_write() - except (OSError, IOError): - self._verify_signing_dir() - _atomic_write() - - def read_file(self, file_name): - path = self.calc_path(file_name) - open_kwargs = {'encoding': 'utf-8'} if six.PY3 else {} - with open(path, 'r', **open_kwargs) as f: - return f.read() - - def calc_path(self, file_name): - return os.path.join(self._directory_name, file_name) - - def _verify_signing_dir(self): - if os.path.isdir(self._directory_name): - if not os.access(self._directory_name, os.W_OK): - raise exc.ConfigurationError( - _('unable to access signing_dir %s') % - self._directory_name) - uid = os.getuid() - if os.stat(self._directory_name).st_uid != uid: - self._log.warning(_LW('signing_dir is not owned by %s'), uid) - current_mode = stat.S_IMODE(os.stat(self._directory_name).st_mode) - if current_mode != stat.S_IRWXU: - self._log.warning( - _LW('signing_dir mode is %(mode)s instead of %(need)s'), - {'mode': oct(current_mode), 'need': oct(stat.S_IRWXU)}) - else: - os.makedirs(self._directory_name, stat.S_IRWXU) diff --git a/keystonemiddleware-moon/keystonemiddleware/auth_token/_user_plugin.py b/keystonemiddleware-moon/keystonemiddleware/auth_token/_user_plugin.py deleted file mode 100644 index 93075c5c..00000000 --- a/keystonemiddleware-moon/keystonemiddleware/auth_token/_user_plugin.py +++ /dev/null @@ -1,193 +0,0 @@ -# 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 keystoneclient.auth.identity import base as base_identity - - -class _TokenData(object): - """An abstraction to show auth_token consumers some of the token contents. - - This is a simplified and cleaned up keystoneclient.access.AccessInfo object - with which services relying on auth_token middleware can find details of - the current token. - """ - - def __init__(self, auth_ref): - self._stored_auth_ref = auth_ref - - @property - def _is_v2(self): - return self._stored_auth_ref.version == 'v2.0' - - @property - def auth_token(self): - """The token data used to authenticate requests. - - :returns: token data. - :rtype: str - """ - return self._stored_auth_ref.auth_token - - @property - def user_id(self): - """The user id associated with the authentication request. - - :rtype: str - """ - return self._stored_auth_ref.user_id - - @property - def user_domain_id(self): - """Returns the domain id of the user associated with the authentication - request. - - :returns: str - """ - # NOTE(jamielennox): v2 AccessInfo returns 'default' for domain_id - # because it can't know that value. We want to return None instead. - if self._is_v2: - return None - - return self._stored_auth_ref.user_domain_id - - @property - def project_id(self): - """The project ID associated with the authentication. - - :rtype: str - """ - return self._stored_auth_ref.project_id - - @property - def project_domain_id(self): - """The domain id of the project associated with the authentication - request. - - :rtype: str - """ - # NOTE(jamielennox): v2 AccessInfo returns 'default' for domain_id - # because it can't know that value. We want to return None instead. - if self._is_v2: - return None - - return self._stored_auth_ref.project_domain_id - - @property - def trust_id(self): - """Returns the trust id associated with the authentication request.. - - :rtype: str - """ - return self._stored_auth_ref.trust_id - - @property - def role_ids(self): - """Role ids of the user associated with the authentication request. - - :rtype: set(str) - """ - return frozenset(self._stored_auth_ref.role_ids or []) - - @property - def role_names(self): - """Role names of the user associated with the authentication request. - - :rtype: set(str) - """ - return frozenset(self._stored_auth_ref.role_names or []) - - @property - def _log_format(self): - roles = ','.join(self.role_names) - return 'user_id %s, project_id %s, roles %s' % (self.user_id, - self.project_id, - roles) - - -class UserAuthPlugin(base_identity.BaseIdentityPlugin): - """The incoming authentication credentials. - - A plugin that represents the incoming user credentials. This can be - consumed by applications. - - This object is not expected to be constructed directly by users. It is - created and passed by auth_token middleware and then can be used as the - authentication plugin when communicating via a session. - """ - - def __init__(self, user_auth_ref, serv_auth_ref): - super(UserAuthPlugin, self).__init__(reauthenticate=False) - - # NOTE(jamielennox): _user_auth_ref and _serv_auth_ref are private - # because this object ends up in the environ that is passed to the - # service, however they are used within auth_token middleware. - self._user_auth_ref = user_auth_ref - self._serv_auth_ref = serv_auth_ref - - self._user_data = None - self._serv_data = None - - @property - def has_user_token(self): - """Did this authentication request contained a user auth token.""" - return self._user_auth_ref is not None - - @property - def user(self): - """Authentication information about the user token. - - Will return None if a user token was not passed with this request. - """ - if not self.has_user_token: - return None - - if not self._user_data: - self._user_data = _TokenData(self._user_auth_ref) - - return self._user_data - - @property - def has_service_token(self): - """Did this authentication request contained a service token.""" - return self._serv_auth_ref is not None - - @property - def service(self): - """Authentication information about the service token. - - Will return None if a user token was not passed with this request. - """ - if not self.has_service_token: - return None - - if not self._serv_data: - self._serv_data = _TokenData(self._serv_auth_ref) - - return self._serv_data - - def get_auth_ref(self, session, **kwargs): - # NOTE(jamielennox): We will always use the auth_ref that was - # calculated by the middleware. reauthenticate=False in __init__ should - # ensure that this function is only called on the first access. - return self._user_auth_ref - - @property - def _log_format(self): - msg = [] - - if self.has_user_token: - msg.append('user: %s' % self.user._log_format) - - if self.has_service_token: - msg.append('service: %s' % self.service._log_format) - - return ' '.join(msg) diff --git a/keystonemiddleware-moon/keystonemiddleware/auth_token/_utils.py b/keystonemiddleware-moon/keystonemiddleware/auth_token/_utils.py deleted file mode 100644 index daed02dd..00000000 --- a/keystonemiddleware-moon/keystonemiddleware/auth_token/_utils.py +++ /dev/null @@ -1,32 +0,0 @@ -# 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 six.moves import urllib - - -def safe_quote(s): - """URL-encode strings that are not already URL-encoded.""" - return urllib.parse.quote(s) if s == urllib.parse.unquote(s) else s - - -class MiniResp(object): - - def __init__(self, error_message, env, headers=[]): - # The HEAD method is unique: it must never return a body, even if - # it reports an error (RFC-2616 clause 9.4). We relieve callers - # from varying the error responses depending on the method. - if env['REQUEST_METHOD'] == 'HEAD': - self.body = [''] - else: - self.body = [error_message.encode()] - self.headers = list(headers) - self.headers.append(('Content-type', 'text/plain')) |