-# Default minimal pipeline
+
+# Default pipeline
[pipeline:heat-api]
-pipeline = versionnegotiation context apiv1app
+pipeline = versionnegotiation ec2authtoken authtoken context apiv1app
# Use the following pipeline for keystone auth
# i.e. in heat-api.conf:
# flavor = keystone
#
[pipeline:heat-api-keystone]
-pipeline = versionnegotiation authtoken auth-context apiv1app
+pipeline = versionnegotiation ec2authtoken authtoken context apiv1app
# Use the following pipeline to enable transparent caching of image files
# i.e. in heat-api.conf:
# flavor = caching
#
[pipeline:heat-api-caching]
-pipeline = versionnegotiation context cache apiv1app
+pipeline = versionnegotiation ec2authtoken authtoken context cache apiv1app
# Use the following pipeline for keystone auth with caching
# i.e. in heat-api.conf:
# flavor = keystone+caching
#
[pipeline:heat-api-keystone+caching]
-pipeline = versionnegotiation authtoken auth-context cache apiv1app
+pipeline = versionnegotiation ec2authtoken authtoken context cache apiv1app
# Use the following pipeline to enable the Image Cache Management API
# i.e. in heat-api.conf:
# flavor = cachemanagement
#
[pipeline:heat-api-cachemanagement]
-pipeline = versionnegotiation context cache cachemanage apiv1app
+pipeline = versionnegotiation ec2authtoken authtoken context cache cachemanage apiv1app
# Use the following pipeline for keystone auth with cache management
# i.e. in heat-api.conf:
# flavor = keystone+cachemanagement
#
[pipeline:heat-api-keystone+cachemanagement]
-pipeline = versionnegotiation authtoken auth-context cache cachemanage apiv1app
+pipeline = versionnegotiation ec2authtoken authtoken auth-context cache cachemanage apiv1app
[app:apiv1app]
paste.app_factory = heat.common.wsgi:app_factory
paste.filter_factory = heat.common.wsgi:filter_factory
heat.filter_factory = heat.common.context:ContextMiddleware
+[filter:ec2authtoken]
+paste.filter_factory = heat.common.wsgi:filter_factory
+heat.filter_factory = heat.api.v1:EC2Token
+auth_uri = http://127.0.0.1:5000/v2.0
+keystone_ec2_uri = http://localhost:5000/v2.0/ec2tokens
+
[filter:authtoken]
-paste.filter_factory = keystone.middleware.auth_token:filter_factory
+paste.filter_factory = heat.common.auth_token:filter_factory
service_protocol = http
service_host = 127.0.0.1
service_port = 5000
auth_host = 127.0.0.1
auth_port = 35357
auth_protocol = http
-auth_uri = http://127.0.0.1:5000/
-admin_tenant_name = %SERVICE_TENANT_NAME%
-admin_user = %SERVICE_USER%
-admin_password = %SERVICE_PASSWORD%
+auth_uri = http://127.0.0.1:5000/v2.0
+
+# These must be set to your local values in order for the token
+# authentication to work.
+admin_tenant_name = admin
+admin_user = admin
+admin_password = verybadpass
[filter:auth-context]
paste.filter_factory = heat.common.wsgi:filter_factory
# License for the specific language governing permissions and limitations
# under the License.
+import json
+import urlparse
+import httplib
import logging
import routes
import gettext
from heat.common import wsgi
from webob import Request
+import webob
+from heat import utils
+from heat.common import context
logger = logging.getLogger(__name__)
+class EC2Token(wsgi.Middleware):
+ """Authenticate an EC2 request with keystone and convert to token."""
+
+ def __init__(self, app, conf, **local_conf):
+ self.conf = local_conf
+ self.application = app
+
+ @webob.dec.wsgify(RequestClass=wsgi.Request)
+ def __call__(self, req):
+ # Read request signature and access id.
+ logger.info("Checking AWS credentials..")
+ try:
+ signature = req.params['Signature']
+ access = req.params['AWSAccessKeyId']
+ except KeyError:
+ # We ignore a key error here so that we can use both
+ # authentication methods. Returning here just means
+ # the user didn't supply AWS authentication and we'll let
+ # the app try native keystone next.
+ logger.info("No AWS credentials found.")
+ return self.application
+
+ logger.info("AWS credentials found, checking against keystone.")
+ # Make a copy of args for authentication and signature verification.
+ auth_params = dict(req.params)
+ # Not part of authentication args
+ auth_params.pop('Signature')
+
+ # Authenticate the request.
+ creds = {'ec2Credentials': {'access': access,
+ 'signature': signature,
+ 'host': req.host,
+ 'verb': req.method,
+ 'path': req.path,
+ 'params': auth_params,
+ }}
+ creds_json = None
+ try:
+ creds_json = json.dumps(creds)
+ except TypeError:
+ creds_json = json.dumps(to_primitive(creds))
+ headers = {'Content-Type': 'application/json'}
+
+ # Disable 'has no x member' pylint error
+ # for httplib and urlparse
+ # pylint: disable-msg=E1101
+
+ logger.info('Authenticating with %s' % self.conf['keystone_ec2_uri'])
+ o = urlparse.urlparse(self.conf['keystone_ec2_uri'])
+ if o.scheme == 'http':
+ conn = httplib.HTTPConnection(o.netloc)
+ else:
+ conn = httplib.HTTPSConnection(o.netloc)
+ conn.request('POST', o.path, body=creds_json, headers=headers)
+ response = conn.getresponse().read()
+ conn.close()
+
+ # NOTE(vish): We could save a call to keystone by
+ # having keystone return token, tenant,
+ # user, and roles from this call.
+
+ result = json.loads(response)
+ try:
+ token_id = result['access']['token']['id']
+ logger.info("AWS authentication successful.")
+ except (AttributeError, KeyError):
+ # FIXME: Should be 404 I think.
+ logger.info("AWS authentication failure.")
+ raise webob.exc.HTTPBadRequest()
+
+ # Authenticated!
+ req.headers['X-Auth-Token'] = token_id
+ req.headers['X-Auth-URL'] = self.conf['auth_uri']
+ return self.application
+
+
class API(wsgi.Router):
"""
HTTPBadRequest)
from heat.common import wsgi
from heat.common import config
+from heat.common import context
+from heat import utils
from heat import rpc
-from heat import context
import heat.rpc.common as rpc_common
"""
Returns the following information for all stacks:
"""
- con = context.get_admin_context()
+ con = req.context
+ parms = dict(req.params)
+
stack_list = rpc.call(con, 'engine',
{'method': 'list_stacks',
- 'args': {'params': dict(req.params)}})
+ 'args': {'params': parms}})
res = {'ListStacksResponse': {
'ListStacksResult': {'StackSummaries': []}}}
"""
Returns the following information for all stacks:
"""
- con = context.get_admin_context()
+ con = req.context
+ parms = dict(req.params)
try:
stack_list = rpc.call(con, 'engine',
{'method': 'show_stack',
'args': {'stack_name': req.params['StackName'],
- 'params': dict(req.params)}})
+ 'params': parms}})
except rpc_common.RemoteError as ex:
return webob.exc.HTTPBadRequest(str(ex))
"""
Returns the following information for all stacks:
"""
- con = context.get_admin_context()
+ con = req.context
+ parms = dict(req.params)
try:
templ = self._get_template(req)
{'method': 'create_stack',
'args': {'stack_name': req.params['StackName'],
'template': stack,
- 'params': dict(req.params)}})
+ 'params': parms}})
except rpc_common.RemoteError as ex:
return webob.exc.HTTPBadRequest(str(ex))
def validate_template(self, req):
- con = context.get_admin_context()
+ con = req.context
+ parms = dict(req.params)
try:
templ = self._get_template(req)
return rpc.call(con, 'engine',
{'method': 'validate_template',
'args': {'template': stack,
- 'params': dict(req.params)}})
+ 'params': parms}})
except rpc_common.RemoteError as ex:
return webob.exc.HTTPBadRequest(str(ex))
"""
Returns the following information for all stacks:
"""
- con = context.get_admin_context()
+ con = req.context
+ parms = dict(req.params)
try:
res = rpc.call(con, 'engine',
{'method': 'delete_stack',
'args': {'stack_name': req.params['StackName'],
- 'params': dict(req.params)}})
+ 'params': parms}})
except rpc_common.RemoteError as ex:
return webob.exc.HTTPBadRequest(str(ex))
"""
Returns the following information for all stacks:
"""
- con = context.get_admin_context()
+ con = req.context
+ parms = dict(req.params)
+
stack_name = req.params.get('StackName', None)
try:
event_res = rpc.call(con, 'engine',
{'method': 'list_events',
- 'args': {'stack_name': stack_name}})
+ 'args': {'stack_name': stack_name,
+ 'params': parms}})
except rpc_common.RemoteError as ex:
return webob.exc.HTTPBadRequest(str(ex))
--- /dev/null
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2010-2012 OpenStack LLC
+#
+# 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 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
+
+
+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, 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, 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()
+
+ 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-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)
+
+
+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)
# License for the specific language governing permissions and limitations
# under the License.
+from heat.openstack.common import local
from heat.common import exception
from heat.common import wsgi
from heat.openstack.common import cfg
from heat.openstack.common import importutils
+from heat.common import utils as heat_utils
+
+
+def generate_request_id():
+ return 'req-' + str(heat_utils.gen_uuid())
class RequestContext(object):
accesses the system, as well as additional request information.
"""
- def __init__(self, auth_tok=None, user=None, tenant=None, roles=None,
+ def __init__(self, auth_token=None, username=None, password=None,
+ tenant=None, tenant_id=None, auth_url=None, roles=None,
is_admin=False, read_only=False, show_deleted=False,
- owner_is_tenant=True):
- self.auth_tok = auth_tok
- self.user = user
+ owner_is_tenant=True, overwrite=True, **kwargs):
+ """
+ :param overwrite: Set to False to ensure that the greenthread local
+ copy of the index is not overwritten.
+
+ :param kwargs: Extra arguments that might be present, but we ignore
+ because they possibly came in from older rpc messages.
+ """
+
+ self.auth_token = auth_token
+ self.username = username
+ self.password = password
self.tenant = tenant
+ self.tenant_id = tenant_id
+ self.auth_url = auth_url
self.roles = roles or []
self.is_admin = is_admin
self.read_only = read_only
self._show_deleted = show_deleted
self.owner_is_tenant = owner_is_tenant
+ if overwrite or not hasattr(local.store, 'context'):
+ self.update_store()
+
+ def update_store(self):
+ local.store.context = self
+
+ def to_dict(self):
+ return {'auth_token': self.auth_token,
+ 'username': self.username,
+ 'password': self.password,
+ 'tenant': self.tenant,
+ 'tenant_id': self.tenant_id,
+ 'auth_url': self.auth_url,
+ 'roles': self.roles,
+ 'is_admin': self.is_admin}
+
+ @classmethod
+ def from_dict(cls, values):
+ return cls(**values)
@property
def owner(self):
return False
+def get_admin_context(read_deleted="no"):
+ return RequestContext(is_admin=True)
+
+
class ContextMiddleware(wsgi.Middleware):
opts = [
tokenauth middleware would have rejected the request, so we must be
using NoAuth. In that case, assume that is_admin=True.
"""
- auth_tok = req.headers.get('X-Auth-Token')
- #
- # hack alert, this is for POC only FIXME properly!
- #
- if False:
- if req.headers.get('X-Identity-Status') == 'Confirmed':
- # 1. Auth-token is passed, check other headers
- user = req.headers.get('X-User-Id')
- tenant = req.headers.get('X-Tenant-Id')
- roles = [r.strip()
- for r in req.headers.get('X-Roles', '').split(',')]
- is_admin = self.conf.admin_role in roles
- else:
- # 2. Indentity-Status not confirmed
- # FIXME(sirp): not sure what the correct behavior in this case
- # is; just raising NotAuthenticated for now
- raise exception.NotAuthenticated()
- else:
- # 3. Auth-token is ommited, assume NoAuth
- user = None
- tenant = None
- roles = []
- is_admin = True
-
- req.context = self.make_context(
- auth_tok=auth_tok, user=user, tenant=tenant, roles=roles,
- is_admin=is_admin)
+ headers = req.headers
+
+ try:
+ """
+ This sets the username/password to the admin user because you
+ need this information in order to perform token authentication.
+ The real 'username' is the 'tenant'.
+
+ We should also check here to see if X-Auth-Token is not set and
+ in that case we should assign the user/pass directly as the real
+ username/password and token as None. 'tenant' should still be
+ the username.
+ """
+
+ token = headers.get('X-Auth-Token')
+ username = headers.get('X-Admin-User')
+ password = headers.get('X-Admin-Pass')
+ tenant = headers.get('X-Tenant')
+ tenant_id = headers.get('X-Tenant-Id')
+ auth_url = headers.get('X-Auth-Url')
+ roles = headers.get('X-Roles')
+ except:
+ raise exception.NotAuthenticated()
+
+ req.context = self.make_context(auth_token=token,
+ tenant=tenant, tenant_id=tenant_id,
+ username=username,
+ password=password,
+ auth_url=auth_url, roles=roles,
+ is_admin=True)
+++ /dev/null
-# vim: tabstop=4 shiftwidth=4 softtabstop=4
-
-# Copyright 2011 OpenStack LLC.
-# Copyright 2010 United States Government as represented by the
-# Administrator of the National Aeronautics and Space Administration.
-# 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.
-
-"""RequestContext: context for requests that persist through all of nova."""
-
-import copy
-import logging
-
-from heat.openstack.common import local
-from heat.openstack.common import utils
-from heat.openstack.common import timeutils
-
-from heat.common import utils as heat_utils
-
-LOG = logging.getLogger(__name__)
-
-
-def generate_request_id():
- return 'req-' + str(heat_utils.gen_uuid())
-
-
-class RequestContext(object):
- """Security context and request information.
-
- Represents the user taking a given action within the system.
-
- """
-
- def __init__(self, user_id, project_id, is_admin=None, read_deleted="no",
- roles=None, remote_address=None, timestamp=None,
- request_id=None, auth_token=None, overwrite=True, **kwargs):
- """
- :param read_deleted: 'no' indicates deleted records are hidden, 'yes'
- indicates deleted records are visible, 'only' indicates that
- *only* deleted records are visible.
-
- :param overwrite: Set to False to ensure that the greenthread local
- copy of the index is not overwritten.
-
- :param kwargs: Extra arguments that might be present, but we ignore
- because they possibly came in from older rpc messages.
- """
- if read_deleted not in ('no', 'yes', 'only'):
- raise ValueError(_("read_deleted can only be one of 'no', "
- "'yes' or 'only', not %r") % read_deleted)
- if kwargs:
- LOG.warn(_('Arguments dropped when creating context: %s') %
- str(kwargs))
-
- self.user_id = user_id
- self.project_id = project_id
- self.roles = roles or []
- self.is_admin = is_admin
- if self.is_admin is None:
- self.is_admin = 'admin' in [x.lower() for x in self.roles]
- elif self.is_admin and 'admin' not in self.roles:
- self.roles.append('admin')
- self.read_deleted = read_deleted
- self.remote_address = remote_address
- if not timestamp:
- timestamp = timeutils.utcnow()
- if isinstance(timestamp, basestring):
- timestamp = heat_utils.parse_strtime(timestamp)
- self.timestamp = timestamp
- if not request_id:
- request_id = generate_request_id()
- self.request_id = request_id
- self.auth_token = auth_token
- if overwrite or not hasattr(local.store, 'context'):
- self.update_store()
-
- def update_store(self):
- local.store.context = self
-
- def to_dict(self):
- return {'user_id': self.user_id,
- 'project_id': self.project_id,
- 'is_admin': self.is_admin,
- 'read_deleted': self.read_deleted,
- 'roles': self.roles,
- 'remote_address': self.remote_address,
- 'timestamp': heat_utils.strtime(self.timestamp),
- 'request_id': self.request_id,
- 'auth_token': self.auth_token}
-
- @classmethod
- def from_dict(cls, values):
- return cls(**values)
-
- def elevated(self, read_deleted=None, overwrite=False):
- """Return a version of this context with admin flag set."""
- context = copy.copy(self)
- context.is_admin = True
-
- if 'admin' not in context.roles:
- context.roles.append('admin')
-
- if read_deleted is not None:
- context.read_deleted = read_deleted
-
- return context
-
-
-def get_admin_context(read_deleted="no"):
- return RequestContext(user_id=None,
- project_id=None,
- is_admin=True,
- read_deleted=read_deleted,
- overwrite=False)
from heat.db import api as db_api
from heat.openstack.common import timeutils
+from novaclient.v1_1 import client
+from novaclient.exceptions import BadRequest
+from novaclient.exceptions import NotFound
+
logger = logging.getLogger('heat.engine.manager')
"""Load configuration options and connect to the hypervisor."""
pass
+ def _authenticate(self, con):
+ """ Authenticate against the 'heat' service. This should be
+ the first call made in an endpoint call. I like to see this
+ done explicitly so that it is clear there is an authentication
+ request at the entry to the call.
+ """
+
+ nova = client.Client(con.username, con.password,
+ con.tenant, con.auth_url,
+ proxy_token=con.auth_token,
+ proxy_tenant_id=con.tenant_id,
+ service_type='heat',
+ service_name='heat')
+ nova.authenticate()
+
def list_stacks(self, context, params):
"""
The list_stacks method is the end point that actually implements
arg1 -> RPC context.
arg2 -> Dict of http request parameters passed in from API side.
"""
- logger.info('context is %s' % context)
+
+ self._authenticate(context)
+
res = {'stacks': []}
stacks = db_api.stack_get_all(None)
if stacks == None:
return res
for s in stacks:
- ps = parser.Stack(s.name, s.raw_template.parsed_template.template,
+ ps = parser.Stack(context, s.name,
+ s.raw_template.parsed_template.template,
s.id, params)
mem = {}
mem['stack_id'] = s.id
arg2 -> Name of the stack you want to see.
arg3 -> Dict of http request parameters passed in from API side.
"""
+ self._authenticate(context)
+
res = {'stacks': []}
s = db_api.stack_get(None, stack_name)
if s:
- ps = parser.Stack(s.name, s.raw_template.parsed_template.template,
+ ps = parser.Stack(context, s.name,
+ s.raw_template.parsed_template.template,
s.id, params)
mem = {}
mem['stack_id'] = s.id
arg4 -> Params passed from API.
"""
logger.info('template is %s' % template)
+
+ self._authenticate(context)
+
if db_api.stack_get(None, stack_name):
return {'Error': 'Stack already exists with that name.'}
# We don't want to reset the stack template, so we are making
# an instance just for validation.
template_copy = deepcopy(template)
- stack_validator = parser.Stack(stack_name, template_copy, 0, params,
- metadata_server=metadata_server)
+ stack_validator = parser.Stack(context, stack_name,
+ template_copy, 0, params,
+ metadata_server=metadata_server)
response = stack_validator.validate()
stack_validator = None
template_copy = None
response['ValidateTemplateResult']['Description']:
return response['ValidateTemplateResult']['Description']
- stack = parser.Stack(stack_name, template, 0, params,
+ stack = parser.Stack(context, stack_name, template, 0, params,
metadata_server=metadata_server)
rt = {}
rt['template'] = template
arg4 -> Params passed from API.
"""
+ self._authenticate(context)
+
logger.info('validate_template')
if template is None:
msg = _("No Template provided.")
return webob.exc.HTTPBadRequest(explanation=msg)
try:
- s = parser.Stack('validate', template, 0, params)
+ s = parser.Stack(context, 'validate', template, 0, params)
except KeyError:
res = 'A Fn::FindInMap operation referenced'\
'a non-existent map [%s]' % sys.exc_value
arg2 -> Name of the stack you want to delete.
arg3 -> Params passed from API.
"""
+
+ self._authenticate(context)
+
st = db_api.stack_get(None, stack_name)
if not st:
return {'Error': 'No stack by that name'}
logger.info('deleting stack %s' % stack_name)
- ps = parser.Stack(st.name, st.raw_template.parsed_template.template,
+ ps = parser.Stack(context, st.name,
+ st.raw_template.parsed_template.template,
st.id, params)
ps.delete()
return None
'ResourceProperties': event.resource_properties,
'ResourceStatus': event.name}
- def list_events(self, context, stack_name):
+ def list_events(self, context, stack_name, params):
"""
The list_events method lists all events associated with a given stack.
arg1 -> RPC context.
arg2 -> Name of the stack you want to get events for.
+ arg3 -> Params passed from API.
"""
+
+ self._authenticate(context)
+
if stack_name is not None:
st = db_api.stack_get(None, stack_name)
if not st:
return {'events': [self.parse_event(e) for e in events]}
def event_create(self, context, event):
+
+ self._authenticate(context)
+
stack_name = event['stack']
resource_name = event['resource']
stack = db_api.stack_get(None, stack_name)
"""
Return the names of the stacks registered with Heat.
"""
+ self._authenticate(context)
+
stacks = db_api.stack_get_all(None)
return [s.name for s in stacks]
"""
Return the resource IDs of the given stack.
"""
+ self._authenticate(context)
+
stack = db_api.stack_get(None, stack_name)
if stack:
return [r.name for r in stack.resources]
"""
Get the metadata for the given resource.
"""
+ self._authenticate(context)
+
s = db_api.stack_get(None, stack_name)
if not s:
return ['stack', None]
"""
Update the metadata for the given resource.
"""
+ self._authenticate(context)
+
s = db_api.stack_get(None, stack_name)
if not s:
return ['stack', None]
DELETE_FAILED = 'DELETE_FAILED'
DELETE_COMPLETE = 'DELETE_COMPLETE'
- def __init__(self, stack_name, template, stack_id=0, parms=None,
+ def __init__(self, context, stack_name, template, stack_id=0, parms=None,
metadata_server=None):
self.id = stack_id
+ self.context = context
self.t = template
self.maps = self.t.get('Mappings', {})
self.outputs = self.t.get('Outputs', {})
if parms != None:
self._apply_user_parameters(parms)
- if isinstance(parms['KeyStoneCreds'], (basestring, unicode)):
- self.creds = eval(parms['KeyStoneCreds'])
- else:
- self.creds = parms['KeyStoneCreds']
-
self.resources = {}
for rname, res in self.t['Resources'].items():
ResourceClass = RESOURCE_CLASSES.get(res['Type'],
if service_type in self._nova:
return self._nova[service_type]
- username = self.stack.creds['username']
- password = self.stack.creds['password']
- tenant = self.stack.creds['tenant']
- auth_url = self.stack.creds['auth_url']
if service_type == 'compute':
service_name = 'nova'
else:
service_name = None
- self._nova[service_type] = client.Client(username, password, tenant,
- auth_url,
+ con = self.stack.context
+ self._nova[service_type] = client.Client(con.username,
+ con.password,
+ con.tenant,
+ con.auth_url,
+ proxy_token=con.auth_token,
+ proxy_tenant_id=con.tenant_id,
service_type=service_type,
service_name=service_name)
return self._nova[service_type]
from webob.exc import Response
from heat.common import wsgi
-from heat import context
+from heat.common import context
from heat import rpc
from eventlet import greenpool
from eventlet import pools
-from heat import context
+from heat.common import context
from heat.common import exception
from heat.common import config
from heat.openstack.common import local
import eventlet
-from heat import context
+from heat.common import context
from heat.common import config
from heat.rpc import common as rpc_common
from heat.common import utils as heat_utils
from heat.common import exception
+from heat.common import context
-from heat import context
from heat import rpc
from heat import version
parameters = {}
params['KeyStoneCreds'] = None
t['Parameters']['KeyName']['Value'] = 'test'
- stack = parser.Stack('test_stack', t, 0, params)
+ stack = parser.Stack(None, 'test_stack', t, 0, params)
self.m.StubOutWithMock(db_api, 'resource_get_by_name_and_stack')
db_api.resource_get_by_name_and_stack(None, 'test_resource_name',\
parameters = {}
params['KeyStoneCreds'] = None
t['Parameters']['KeyName']['Value'] = 'test'
- stack = parser.Stack('test_stack', t, 0, params)
+ stack = parser.Stack(None, 'test_stack', t, 0, params)
self.m.StubOutWithMock(db_api, 'resource_get_by_name_and_stack')
db_api.resource_get_by_name_and_stack(None, 'test_resource_name',\
f.close()
params = {}
parameters = {}
- params['KeyStoneCreds'] = None
t['Parameters']['KeyName']['Value'] = 'test'
- stack = parser.Stack(stack_name, t, 0, params)
+ stack = parser.Stack(None, stack_name, t, 0, params)
self.m.StubOutWithMock(instances.Instance, 'nova')
instances.Instance.nova().AndReturn(self.fc)
instances.Instance.nova().AndReturn(self.fc)
assert(result['ResourceProperties']['InstanceType'] == 'm1.large')
def test_stack_list(self):
+ self.m.StubOutWithMock(manager.EngineManager, '_authenticate')
+ manager.EngineManager._authenticate(None).AndReturn(True)
stack = self.start_wordpress_stack('test_stack_list')
rt = {}
rt['template'] = stack.t
t = json.loads(f.read())
params = {}
parameters = {}
- params['KeyStoneCreds'] = None
t['Parameters']['KeyName']['Value'] = 'test'
- stack = parser.Stack('test_stack_list', t, 0, params)
+ stack = parser.Stack(None, 'test_stack_list', t, 0, params)
man = manager.EngineManager()
sl = man.list_stacks(None, params)
def test_validate_volumeattach_valid(self):
t = json.loads(test_template_volumeattach % 'vdq')
+ self.m.StubOutWithMock(managers.EngineManager, '_authenticate')
+ managers.EngineManager._authenticate(None).AndReturn(True)
params = {}
- params['KeyStoneCreds'] = None
- stack = parser.Stack('test_stack', t, 0, params)
+ stack = parser.Stack(None, 'test_stack', t, 0, params)
self.m.StubOutWithMock(db_api, 'resource_get_by_name_and_stack')
db_api.resource_get_by_name_and_stack(None, 'test_resource_name',\
def test_validate_volumeattach_invalid(self):
t = json.loads(test_template_volumeattach % 'sda')
+ self.m.StubOutWithMock(managers.EngineManager, '_authenticate')
+ managers.EngineManager._authenticate(None).AndReturn(True)
params = {}
- params['KeyStoneCreds'] = None
- stack = parser.Stack('test_stack', t, 0, params)
+ stack = parser.Stack(None, 'test_stack', t, 0, params)
self.m.StubOutWithMock(db_api, 'resource_get_by_name_and_stack')
db_api.resource_get_by_name_and_stack(None, 'test_resource_name',\
t = json.loads(test_template_ref % 'WikiDatabase')
t['Parameters']['KeyName']['Value'] = 'test'
params = {}
- params['KeyStoneCreds'] = None
+ self.m.StubOutWithMock(managers.EngineManager, '_authenticate')
+ managers.EngineManager._authenticate(None).AndReturn(True)
self.m.StubOutWithMock(instances.Instance, 'nova')
instances.Instance.nova().AndReturn(self.fc)
t = json.loads(test_template_ref % 'WikiDatabasez')
t['Parameters']['KeyName']['Value'] = 'test'
params = {}
- params['KeyStoneCreds'] = None
+ self.m.StubOutWithMock(managers.EngineManager, '_authenticate')
+ managers.EngineManager._authenticate(None).AndReturn(True)
self.m.StubOutWithMock(instances.Instance, 'nova')
instances.Instance.nova().AndReturn(self.fc)
t = json.loads(test_template_findinmap_valid)
t['Parameters']['KeyName']['Value'] = 'test'
params = {}
- params['KeyStoneCreds'] = None
+ self.m.StubOutWithMock(managers.EngineManager, '_authenticate')
+ managers.EngineManager._authenticate(None).AndReturn(True)
self.m.StubOutWithMock(instances.Instance, 'nova')
instances.Instance.nova().AndReturn(self.fc)
t = json.loads(test_template_findinmap_invalid)
t['Parameters']['KeyName']['Value'] = 'test'
params = {}
- params['KeyStoneCreds'] = None
+ self.m.StubOutWithMock(managers.EngineManager, '_authenticate')
+ managers.EngineManager._authenticate(None).AndReturn(True)
self.m.StubOutWithMock(instances.Instance, 'nova')
instances.Instance.nova().AndReturn(self.fc)