Files
ec2-api/ec2api/metadata/__init__.py
Andrey Pavlov 47d0a7c35b rework v4 auth.
workaround webob bug.
unify keystone auth with current nova code.
make secret_key lazy loaded.

Change-Id: I475e422af28b8b14a57cbb86901b5059e457d3a0
2015-02-03 17:00:25 +03:00

275 lines
10 KiB
Python

# Copyright 2014
# The Cloudscaling Group, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import hashlib
import hmac
import posixpath
import urlparse
import httplib2
from keystoneclient.v2_0 import client as keystone_client
from oslo.config import cfg
import six
import webob
from ec2api import context as ec2context
from ec2api import exception
from ec2api.metadata import api
from ec2api.openstack.common import gettextutils as textutils
from ec2api.openstack.common.gettextutils import _
from ec2api.openstack.common import log as logging
from ec2api import utils
from ec2api import wsgi
LOG = logging.getLogger(__name__)
CONF = cfg.CONF
CONF.import_opt('use_forwarded_for', 'ec2api.api.auth')
metadata_opts = [
cfg.StrOpt('nova_metadata_ip',
default='127.0.0.1',
help=_("IP address used by Nova metadata server.")),
cfg.IntOpt('nova_metadata_port',
default=8775,
help=_("TCP Port used by Nova metadata server.")),
cfg.StrOpt('nova_metadata_protocol',
default='http',
choices=['http', 'https'],
help=_("Protocol to access nova metadata, http or https")),
cfg.BoolOpt('nova_metadata_insecure',
default=False,
help=_("Allow to perform insecure SSL (https) requests to "
"nova metadata")),
cfg.StrOpt('auth_ca_cert',
help=_("Certificate Authority public key (CA cert) "
"file for ssl")),
cfg.StrOpt('nova_client_cert',
default='',
help=_("Client certificate for nova metadata api server.")),
cfg.StrOpt('nova_client_priv_key',
default='',
help=_("Private key of client certificate.")),
cfg.StrOpt('admin_user',
help=_("Admin user")),
cfg.StrOpt('admin_password',
help=_("Admin password"),
secret=True),
cfg.StrOpt('admin_tenant_name',
help=_("Admin tenant name")),
cfg.StrOpt('metadata_proxy_shared_secret',
default='',
help=_('Shared secret to sign instance-id request'),
secret=True),
]
CONF.register_opts(metadata_opts, group='metadata')
class MetadataRequestHandler(wsgi.Application):
"""Serve metadata."""
@webob.dec.wsgify(RequestClass=wsgi.Request)
def __call__(self, req):
LOG.debug('Request: %s', req)
path = req.path_info
if path == '' or path[0] != '/':
path = '/' + path
path = posixpath.normpath(path)
path_tokens = path.split('/')[1:]
if path_tokens in ([''], ['ec2']):
resp = api.get_version_list()
return self._add_response_data(req.response, resp)
try:
if path_tokens[0] == 'openstack':
return self._proxy_request(req)
elif path_tokens[0] == 'ec2':
path_tokens = path_tokens[1:]
resp = self._get_metadata(req, path_tokens)
return self._add_response_data(req.response, resp)
except exception.EC2MetadataNotFound:
return webob.exc.HTTPNotFound()
except Exception:
LOG.exception(textutils._LE("Unexpected error."))
msg = _('An unknown error has occurred. '
'Please try your request again.')
return webob.exc.HTTPInternalServerError(explanation=unicode(msg))
def _proxy_request(self, req):
headers = self._build_proxy_request_headers(req)
nova_ip_port = '%s:%s' % (CONF.metadata.nova_metadata_ip,
CONF.metadata.nova_metadata_port)
url = urlparse.urlunsplit((
CONF.metadata.nova_metadata_protocol,
nova_ip_port,
req.path_info,
req.query_string,
''))
h = httplib2.Http(
ca_certs=CONF.metadata.auth_ca_cert,
disable_ssl_certificate_validation=(
CONF.metadata.nova_metadata_insecure)
)
if (CONF.metadata.nova_client_cert and
CONF.metadata.nova_client_priv_key):
h.add_certificate(CONF.metadata.nova_client_priv_key,
CONF.metadata.nova_client_cert,
nova_ip_port)
resp, content = h.request(url, method=req.method, headers=headers,
body=req.body)
if resp.status == 200:
LOG.debug(str(resp))
req.response.content_type = resp['content-type']
req.response.body = content
return req.response
elif resp.status == 403:
LOG.warn(textutils._LW(
'The remote metadata server responded with Forbidden. This '
'response usually occurs when shared secrets do not match.'
))
return webob.exc.HTTPForbidden()
elif resp.status == 400:
return webob.exc.HTTPBadRequest()
elif resp.status == 404:
return webob.exc.HTTPNotFound()
elif resp.status == 409:
return webob.exc.HTTPConflict()
elif resp.status == 500:
msg = _(
'Remote metadata server experienced an internal server error.'
)
LOG.warn(msg)
return webob.exc.HTTPInternalServerError(explanation=unicode(msg))
else:
raise Exception(_('Unexpected response code: %s') % resp.status)
def _build_proxy_request_headers(self, req):
if req.headers.get('X-Instance-ID'):
return req.headers
remote_ip = self._get_remote_ip(req)
context = self._get_context()
instance_id, project_id = (
api.get_os_instance_and_project_id(context, remote_ip))
return {
'X-Forwarded-For': remote_ip,
'X-Instance-ID': instance_id,
'X-Tenant-ID': project_id,
'X-Instance-ID-Signature': self._sign_instance_id(instance_id),
}
def _get_remote_ip(self, req):
remote_ip = req.remote_addr
if CONF.use_forwarded_for:
remote_ip = req.headers.get('X-Forwarded-For', remote_ip)
if not remote_ip:
raise exception.EC2MetadataInvalidAddress()
return remote_ip
def _get_context(self):
# TODO(ft): make authentification token reusable
keystone = keystone_client.Client(
username=CONF.metadata.admin_user,
password=CONF.metadata.admin_password,
tenant_name=CONF.metadata.admin_tenant_name,
auth_url=CONF.keystone_url,
)
service_catalog = keystone.service_catalog.get_data()
return ec2context.RequestContext(
keystone.auth_user_id,
keystone.auth_tenant_id,
auth_token=keystone.auth_token,
service_catalog=service_catalog,
is_admin=True,
cross_tenants=True)
def _sign_instance_id(self, instance_id):
return hmac.new(CONF.metadata.metadata_proxy_shared_secret,
instance_id,
hashlib.sha256).hexdigest()
def _get_metadata(self, req, path_tokens):
context = self._get_context()
if req.headers.get('X-Instance-ID'):
os_instance_id, project_id, remote_ip = (
self._unpack_request_attributes(req))
else:
remote_ip = self._get_remote_ip(req)
os_instance_id, project_id = (
api.get_os_instance_and_project_id(context, remote_ip))
# NOTE(ft): substitute project_id for context to instance's one.
# It's needed for correct describe and auto update DB operations.
# It doesn't affect operations via OpenStack's clients because
# these clients use auth_token field only
context.project_id = project_id
return api.get_metadata_item(context, path_tokens, os_instance_id,
remote_ip)
def _unpack_request_attributes(self, req):
os_instance_id = req.headers.get('X-Instance-ID')
project_id = req.headers.get('X-Tenant-ID')
signature = req.headers.get('X-Instance-ID-Signature')
remote_ip = req.headers.get('X-Forwarded-For')
if not remote_ip:
raise exception.EC2MetadataInvalidAddress()
if os_instance_id is None:
msg = _('X-Instance-ID header is missing from request.')
elif project_id is None:
msg = _('X-Tenant-ID header is missing from request.')
elif not isinstance(os_instance_id, six.string_types):
msg = _('Multiple X-Instance-ID headers found within request.')
elif not isinstance(project_id, six.string_types):
msg = _('Multiple X-Tenant-ID headers found within request.')
else:
msg = None
if msg:
raise webob.exc.HTTPBadRequest(explanation=msg)
expected_signature = hmac.new(
CONF.metadata.metadata_proxy_shared_secret,
os_instance_id,
hashlib.sha256).hexdigest()
if not utils.constant_time_compare(expected_signature, signature):
LOG.warning(textutils._LW(
'X-Instance-ID-Signature: %(signature)s does '
'not match the expected value: '
'%(expected_signature)s for id: '
'%(instance_id)s. Request From: '
'%(remote_ip)s'),
{'signature': signature,
'expected_signature': expected_signature,
'instance_id': os_instance_id,
'remote_ip': remote_ip})
msg = _('Invalid proxy request signature.')
raise webob.exc.HTTPForbidden(explanation=msg)
return os_instance_id, project_id, remote_ip
def _add_response_data(self, response, data):
if isinstance(data, six.text_type):
response.text = data
else:
response.body = data
response.content_type = 'text/plain'
return response