# See the License for the specific language governing permissions and
# limitations under the License.
-"""
-TOKEN-BASED AUTH MIDDLEWARE
-
-This WSGI component:
-
-* Verifies that incoming client requests have valid tokens by validating
- tokens with the auth service.
-* Rejects unauthenticated requests UNLESS it 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, tenant, etc
-
-Refer to: http://keystone.openstack.org/middlewarearchitecture.html
-
-HEADERS
--------
-
-* Headers starting with HTTP\_ is a standard http header
-* Headers starting with HTTP_X is an extended http header
-
-Coming in from initial call from client or customer
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-HTTP_X_AUTH_TOKEN
- The client token being passed in.
-
-HTTP_X_STORAGE_TOKEN
- The client token being passed in (legacy Rackspace use) to support
- swift/cloud files
-
-Used for communication between components
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-WWW-Authenticate
- HTTP header returned to a user indicating which endpoint to use
- to retrieve a new token
-
-What we add to the request for use by the OpenStack service
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-HTTP_X_IDENTITY_STATUS
- '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
-
-HTTP_X_TENANT_ID
- Identity service managed unique identifier, string
-
-HTTP_X_TENANT_NAME
- Unique tenant identifier, string
-
-HTTP_X_USER_ID
- Identity-service managed unique identifier, string
-
-HTTP_X_USER_NAME
- Unique user identifier, string
-
-HTTP_X_ROLES
- Comma delimited list of case-sensitive Roles
-
-HTTP_X_TENANT
- *Deprecated* in favor of HTTP_X_TENANT_ID and HTTP_X_TENANT_NAME
- Keystone-assigned unique identifier, deprecated
-
-HTTP_X_USER
- *Deprecated* in favor of HTTP_X_USER_ID and HTTP_X_USER_NAME
- Unique user name, string
-
-HTTP_X_ROLE
- *Deprecated* in favor of HTTP_X_ROLES
- This is being renamed, and the new header contains the same data.
-
-"""
-
-import httplib
-import json
import logging
-import time
-
-import webob
-import webob.exc
-
+from keystoneclient.middleware import auth_token
LOG = logging.getLogger(__name__)
-class InvalidUserToken(Exception):
- pass
-
-
-class ServiceError(Exception):
- pass
-
-
-class AuthProtocol(object):
- """Auth Middleware that handles authenticating client calls."""
-
- def __init__(self, app, conf):
- LOG.info('Starting keystone auth_token middleware')
- self.conf = conf
- self.app = app
-
- # delay_auth_decision means we still allow unauthenticated requests
- # through and we let the downstream service make the final decision
- self.delay_auth_decision = int(conf.get('delay_auth_decision', 0))
-
- # where to find the auth service (we use this to validate tokens)
- self.auth_host = conf.get('auth_host')
- self.auth_port = int(conf.get('auth_port', 35357))
- auth_protocol = conf.get('auth_protocol', 'https')
- if auth_protocol == 'http':
- self.http_client_class = httplib.HTTPConnection
- else:
- self.http_client_class = httplib.HTTPSConnection
-
- default_auth_uri = '%s://%s:%s' % (auth_protocol,
- self.auth_host,
- self.auth_port)
- self.auth_uri = conf.get('auth_uri', default_auth_uri)
-
- # Credentials used to verify this component with the Auth service since
- # validating tokens is a privileged call
- self.admin_token = conf.get('admin_token')
- self.admin_user = conf.get('admin_user')
- self.admin_password = conf.get('admin_password')
- self.admin_tenant_name = conf.get('admin_tenant_name', 'admin')
-
- # Token caching via memcache
- self._cache = None
- self._iso8601 = None
- memcache_servers = conf.get('memcache_servers')
- # By default the token will be cached for 5 minutes
- self.token_cache_time = conf.get('token_cache_time', 300)
- if memcache_servers:
- try:
- import memcache
- import iso8601
- LOG.info('Using memcache for caching token')
- self._cache = memcache.Client(memcache_servers.split(','))
- self._iso8601 = iso8601
- except NameError as e:
- LOG.warn('disabled caching due to missing libraries %s', e)
-
- def __call__(self, env, start_response):
- """Handle incoming request.
-
- Authenticate send downstream on success. Reject request if
- we can't authenticate.
-
- """
- LOG.debug('Authenticating user token')
- try:
- self._remove_auth_headers(env)
- user_token = self._get_user_token_from_header(env)
- token_info = self._validate_user_token(user_token)
- user_headers = self._build_user_headers(token_info)
- self._add_headers(env, user_headers)
- return self.app(env, start_response)
-
- except InvalidUserToken:
- if self.delay_auth_decision:
- LOG.info('Invalid user token - deferring reject downstream')
- self._add_headers(env, {'X-Identity-Status': 'Invalid'})
- return self.app(env, start_response)
- else:
- LOG.info('Invalid user token - rejecting request')
- return self._reject_request(env, start_response)
-
- except ServiceError as e:
- LOG.critical('Unable to obtain admin token: %s' % e)
- resp = webob.exc.HTTPServiceUnavailable()
- return resp(env, start_response)
-
- def _remove_auth_headers(self, env):
- """Remove headers so a user can't fake authentication.
-
- :param env: wsgi request environment
-
- """
- auth_headers = (
- 'X-Identity-Status',
- 'X-Tenant-Id',
- 'X-Tenant-Name',
- 'X-User-Id',
- 'X-User-Name',
- 'X-Roles',
- # Deprecated
- 'X-User',
- 'X-Tenant',
- 'X-Role',
- )
- LOG.debug('Removing headers from request environment: %s' %
- ','.join(auth_headers))
- self._remove_headers(env, auth_headers)
-
- def _get_user_token_from_header(self, env):
- """Get token id from request.
-
- :param env: wsgi request environment
- :return token id
- :raises InvalidUserToken if no token is provided in request
-
- """
- token = self._get_header(env, 'X-Auth-Token',
- self._get_header(env, 'X-Storage-Token'))
- if token:
- return token
- else:
- LOG.warn("Unable to find authentication token in headers: %s", env)
- raise InvalidUserToken('Unable to find token in headers')
-
- def _reject_request(self, env, start_response):
- """Redirect client to auth server.
-
- :param env: wsgi request environment
- :param start_response: wsgi response callback
- :returns HTTPUnauthorized http response
-
- """
- headers = [('WWW-Authenticate', 'Keystone uri=\'%s\'' % self.auth_uri)]
- resp = webob.exc.HTTPUnauthorized('Authentication required', headers)
- return resp(env, start_response)
-
- def get_admin_token(self):
- """Return admin token, possibly fetching a new one.
-
- :return admin token id
- :raise ServiceError when unable to retrieve token from keystone
-
- """
- if not self.admin_token:
- self.admin_token = self._request_admin_token()
-
- return self.admin_token
-
- def _get_http_connection(self):
- return self.http_client_class(self.auth_host, self.auth_port)
-
- def _json_request(self, method, path, body=None, additional_headers=None):
- """HTTP request helper used to make json requests.
-
- :param method: http method
- :param path: relative request url
- :param body: dict to encode to json as request body. Optional.
- :param additional_headers: dict of additional headers to send with
- http request. Optional.
- :return (http response object, response body parsed as json)
- :raise ServerError when unable to communicate with keystone
-
- """
- conn = self._get_http_connection()
-
- kwargs = {
- 'headers': {
- 'Content-type': 'application/json',
- 'Accept': 'application/json',
- },
- }
-
- if additional_headers:
- kwargs['headers'].update(additional_headers)
-
- if body:
- kwargs['body'] = json.dumps(body)
-
- try:
- conn.request(method, path, **kwargs)
- response = conn.getresponse()
- body = response.read()
- except Exception as e:
- LOG.error('HTTP connection exception: %s' % e)
- raise ServiceError('Unable to communicate with keystone')
- finally:
- conn.close()
-
- try:
- data = json.loads(body)
- except ValueError:
- LOG.debug('Keystone did not return json-encoded body')
- data = {}
-
- return response, data
-
- def _request_admin_token(self):
- """Retrieve new token as admin user from keystone.
-
- :return token id upon success
- :raises ServerError when unable to communicate with keystone
-
- """
- params = {
- 'auth': {
- 'passwordCredentials': {
- 'username': self.admin_user,
- 'password': self.admin_password,
- },
- 'tenantName': self.admin_tenant_name,
- }
- }
-
- response, data = self._json_request('POST',
- '/v2.0/tokens',
- body=params)
-
- try:
- token = data['access']['token']['id']
- assert token
- return token
- except (AssertionError, KeyError):
- LOG.warn("Unexpected response from keystone service: %s", data)
- raise ServiceError('invalid json response')
-
- def _validate_user_token(self, user_token, retry=True):
- """Authenticate user token with keystone.
-
- :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.
- :return token object received from keystone on success
- :raise InvalidUserToken if token is rejected
- :raise ServiceError if unable to authenticate token
-
- """
- cached = self._cache_get(user_token)
- if cached:
- return cached
-
- headers = {'X-Auth-Token': self.get_admin_token()}
- response, data = self._json_request('GET',
- '/v2.0/tokens/%s' % user_token,
- additional_headers=headers)
-
- if response.status == 200:
- self._cache_put(user_token, data)
- return data
- if response.status == 404:
- # FIXME(ja): I'm assuming the 404 status means that user_token is
- # invalid - not that the admin_token is invalid
- self._cache_store_invalid(user_token)
- LOG.warn("Authorization failed for token %s", user_token)
- raise InvalidUserToken('Token authorization failed')
- if response.status == 401:
- LOG.info('Keystone rejected admin token %s, resetting', headers)
- self.admin_token = None
- else:
- LOG.error('Bad response code while validating token: %s' %
- response.status)
- if retry:
- LOG.info('Retrying validation')
- return self._validate_user_token(user_token, False)
- else:
- LOG.warn("Invalid user token: %s. Keystone response: %s.",
- user_token, data)
-
- raise InvalidUserToken()
-
+class AuthProtocol(auth_token.AuthProtocol):
+ """
+ Subclass of keystoneclient auth_token middleware which also
+ sets the 'X-Auth-Url' header to the value specified in the config.
+ """
def _build_user_headers(self, token_info):
- """Convert token object into headers.
-
- Build headers that represent authenticated user:
- * X_IDENTITY_STATUS: Confirmed or Invalid
- * X_TENANT_ID: id of tenant if tenant is present
- * X_TENANT_NAME: name of tenant if tenant is present
- * X_USER_ID: id of user
- * X_USER_NAME: name of user
- * X_ROLES: list of roles
-
- Additional (deprecated) headers include:
- * X_USER: name of user
- * X_TENANT: For legacy compatibility before we had ID and Name
- * X_ROLE: list of roles
-
- :param token_info: token object returned by keystone on authentication
- :raise InvalidUserToken when unable to parse token object
-
- """
- user = token_info['access']['user']
- token = token_info['access']['token']
- roles = ','.join([role['name'] for role in user.get('roles', [])])
-
- def get_tenant_info():
- """Returns a (tenant_id, tenant_name) tuple from context."""
- def essex():
- """Essex puts the tenant ID and name on the token."""
- return (token['tenant']['id'], token['tenant']['name'])
-
- def pre_diablo():
- """Pre-diablo, Keystone only provided tenantId."""
- return (token['tenantId'], token['tenantId'])
-
- def default_tenant():
- """Assume the user's default tenant."""
- return (user['tenantId'], user['tenantName'])
-
- for method in [essex, pre_diablo, default_tenant]:
- try:
- return method()
- except KeyError:
- pass
-
- raise InvalidUserToken('Unable to determine tenancy.')
-
- tenant_id, tenant_name = get_tenant_info()
-
- user_id = user['id']
- user_name = user['name']
-
- return {
- 'X-Identity-Status': 'Confirmed',
- 'X-Tenant-Id': tenant_id,
- 'X-Tenant-Name': tenant_name,
- 'X-User-Id': user_id,
- 'X-User-Name': user_name,
- 'X-Roles': roles,
- # Deprecated
- 'X-User': user_name,
- 'X-Tenant': tenant_name,
- 'X-Role': roles,
- 'X-Admin-User': self.admin_user,
- 'X-Admin-Pass': self.admin_password,
- 'X-Admin-Tenant-Name': self.admin_tenant_name,
- 'X-Auth-Url': self.conf['auth_uri'],
- }
-
- def _header_to_env_var(self, key):
- """Convert header to wsgi env variable.
-
- :param key: http header name (ex. 'X-Auth-Token')
- :return wsgi env variable name (ex. 'HTTP_X_AUTH_TOKEN')
-
- """
- return 'HTTP_%s' % (key.replace('-', '_').upper())
-
- def _add_headers(self, env, headers):
- """Add http headers to environment."""
- for (k, v) in headers.iteritems():
- env_key = self._header_to_env_var(k)
- env[env_key] = v
-
- def _remove_headers(self, env, keys):
- """Remove http headers from environment."""
- for k in keys:
- env_key = self._header_to_env_var(k)
- try:
- del env[env_key]
- except KeyError:
- pass
-
- def _get_header(self, env, key, default=None):
- """Get http header from environment."""
- env_key = self._header_to_env_var(key)
- return env.get(env_key, default)
-
- def _cache_get(self, token):
- """Return token information from cache.
-
- If token is invalid raise InvalidUserToken
- return token only if fresh (not expired).
- """
- if self._cache and token:
- key = 'tokens/%s' % token
- cached = self._cache.get(key)
- if cached == 'invalid':
- LOG.debug('Cached Token %s is marked unauthorized', token)
- raise InvalidUserToken('Token authorization failed')
- if cached:
- data, expires = cached
- if time.time() < float(expires):
- LOG.debug('Returning cached token %s', token)
- return data
- else:
- LOG.debug('Cached Token %s seems expired', token)
-
- def _cache_put(self, token, data):
- """Put token data into the cache.
-
- Stores the parsed expire date in cache allowing
- quick check of token freshness on retrieval.
- """
- if self._cache and data:
- key = 'tokens/%s' % token
- if 'token' in data.get('access', {}):
- timestamp = data['access']['token']['expires']
- expires = self._iso8601.parse_date(timestamp).strftime('%s')
- else:
- LOG.error('invalid token format')
- return
- LOG.debug('Storing %s token in memcache', token)
- self._cache.set(key,
- (data, expires),
- time=self.token_cache_time)
-
- def _cache_store_invalid(self, token):
- """Store invalid token in cache."""
- if self._cache:
- key = 'tokens/%s' % token
- LOG.debug('Marking token %s as unauthorized in memcache', token)
- self._cache.set(key,
- 'invalid',
- time=self.token_cache_time)
+ rval = super(AuthProtocol, self)._build_user_headers(token_info)
+ rval['X-Auth-Url'] = self.auth_uri
+ rval['X-Admin-User'] = self.admin_user
+ rval['X-Admin-Pass'] = self.admin_password
+ rval['X-Admin-Tenant-Name'] = self.admin_tenant_name
+ return rval
def filter_factory(global_conf, **local_conf):