Even in case of DEBUG level logging credentials (especially those that give admin level access) should not be saved into log files. This way of handling it has the side effect that if someone uses password "password", it will be replaced in another place too... but password "password" or some other keyword that can be found in the request itself was a pretty bad idea to begin with. Shell utilities are not affected and the verbose mode will still display the passwords to make debugging easy. Implements: blueprint limit-credentials-logging Change-Id: I50d0ebbfbd44c7a5b162d9334b4fdbda67e5c28d
283 lines
11 KiB
Python
283 lines
11 KiB
Python
# Copyright 2012 OpenStack LLC.
|
|
# 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.
|
|
#
|
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
|
|
|
try:
|
|
import json
|
|
except ImportError:
|
|
import simplejson as json
|
|
import logging
|
|
import os
|
|
import urlparse
|
|
# Python 2.5 compat fix
|
|
if not hasattr(urlparse, 'parse_qsl'):
|
|
import cgi
|
|
urlparse.parse_qsl = cgi.parse_qsl
|
|
|
|
import httplib2
|
|
|
|
from neutronclient.common import exceptions
|
|
from neutronclient.common import utils
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
if os.environ.get('NEUTRONCLIENT_DEBUG'):
|
|
ch = logging.StreamHandler()
|
|
_logger.setLevel(logging.DEBUG)
|
|
_logger.addHandler(ch)
|
|
|
|
|
|
class ServiceCatalog(object):
|
|
"""Helper methods for dealing with a Keystone Service Catalog."""
|
|
|
|
def __init__(self, resource_dict):
|
|
self.catalog = resource_dict
|
|
|
|
def get_token(self):
|
|
"""Fetch token details fron service catalog."""
|
|
token = {'id': self.catalog['access']['token']['id'],
|
|
'expires': self.catalog['access']['token']['expires'], }
|
|
try:
|
|
token['user_id'] = self.catalog['access']['user']['id']
|
|
token['tenant_id'] = (
|
|
self.catalog['access']['token']['tenant']['id'])
|
|
except Exception:
|
|
# just leave the tenant and user out if it doesn't exist
|
|
pass
|
|
return token
|
|
|
|
def url_for(self, attr=None, filter_value=None,
|
|
service_type='network', endpoint_type='publicURL'):
|
|
"""Fetch the URL from the Neutron service for
|
|
a particular endpoint type. If none given, return
|
|
publicURL.
|
|
"""
|
|
|
|
catalog = self.catalog['access'].get('serviceCatalog', [])
|
|
matching_endpoints = []
|
|
for service in catalog:
|
|
if service['type'] != service_type:
|
|
continue
|
|
|
|
endpoints = service['endpoints']
|
|
for endpoint in endpoints:
|
|
if not filter_value or endpoint.get(attr) == filter_value:
|
|
matching_endpoints.append(endpoint)
|
|
|
|
if not matching_endpoints:
|
|
raise exceptions.EndpointNotFound()
|
|
elif len(matching_endpoints) > 1:
|
|
raise exceptions.AmbiguousEndpoints(message=matching_endpoints)
|
|
else:
|
|
if endpoint_type not in matching_endpoints[0]:
|
|
raise exceptions.EndpointTypeNotFound(message=endpoint_type)
|
|
|
|
return matching_endpoints[0][endpoint_type]
|
|
|
|
|
|
class HTTPClient(httplib2.Http):
|
|
"""Handles the REST calls and responses, include authn."""
|
|
|
|
USER_AGENT = 'python-neutronclient'
|
|
|
|
def __init__(self, username=None, tenant_name=None, tenant_id=None,
|
|
password=None, auth_url=None,
|
|
token=None, region_name=None, timeout=None,
|
|
endpoint_url=None, insecure=False,
|
|
endpoint_type='publicURL',
|
|
auth_strategy='keystone', ca_cert=None, log_credentials=False,
|
|
**kwargs):
|
|
super(HTTPClient, self).__init__(timeout=timeout, ca_certs=ca_cert)
|
|
|
|
self.username = username
|
|
self.tenant_name = tenant_name
|
|
self.tenant_id = tenant_id
|
|
self.password = password
|
|
self.auth_url = auth_url.rstrip('/') if auth_url else None
|
|
self.endpoint_type = endpoint_type
|
|
self.region_name = region_name
|
|
self.auth_token = token
|
|
self.content_type = 'application/json'
|
|
self.endpoint_url = endpoint_url
|
|
self.auth_strategy = auth_strategy
|
|
self.log_credentials = log_credentials
|
|
# httplib2 overrides
|
|
self.disable_ssl_certificate_validation = insecure
|
|
|
|
def _cs_request(self, *args, **kwargs):
|
|
kargs = {}
|
|
kargs.setdefault('headers', kwargs.get('headers', {}))
|
|
kargs['headers']['User-Agent'] = self.USER_AGENT
|
|
|
|
if 'content_type' in kwargs:
|
|
kargs['headers']['Content-Type'] = kwargs['content_type']
|
|
kargs['headers']['Accept'] = kwargs['content_type']
|
|
else:
|
|
kargs['headers']['Content-Type'] = self.content_type
|
|
kargs['headers']['Accept'] = self.content_type
|
|
|
|
if 'body' in kwargs:
|
|
kargs['body'] = kwargs['body']
|
|
args = utils.safe_encode_list(args)
|
|
kargs = utils.safe_encode_dict(kargs)
|
|
|
|
if self.log_credentials:
|
|
log_kargs = kargs
|
|
else:
|
|
log_kargs = self._strip_credentials(kargs)
|
|
|
|
utils.http_log_req(_logger, args, log_kargs)
|
|
try:
|
|
resp, body = self.request(*args, **kargs)
|
|
except httplib2.SSLHandshakeError as e:
|
|
raise exceptions.SslCertificateValidationError(reason=e)
|
|
except Exception as e:
|
|
# Wrap the low-level connection error (socket timeout, redirect
|
|
# limit, decompression error, etc) into our custom high-level
|
|
# connection exception (it is excepted in the upper layers of code)
|
|
raise exceptions.ConnectionFailed(reason=e)
|
|
utils.http_log_resp(_logger, resp, body)
|
|
status_code = self.get_status_code(resp)
|
|
if status_code == 401:
|
|
raise exceptions.Unauthorized(message=body)
|
|
elif status_code == 403:
|
|
raise exceptions.Forbidden(message=body)
|
|
return resp, body
|
|
|
|
def _strip_credentials(self, kwargs):
|
|
if kwargs.get('body') and self.password:
|
|
log_kwargs = kwargs.copy()
|
|
log_kwargs['body'] = kwargs['body'].replace(self.password,
|
|
'REDACTED')
|
|
return log_kwargs
|
|
else:
|
|
return kwargs
|
|
|
|
def authenticate_and_fetch_endpoint_url(self):
|
|
if not self.auth_token:
|
|
self.authenticate()
|
|
elif not self.endpoint_url:
|
|
self.endpoint_url = self._get_endpoint_url()
|
|
|
|
def do_request(self, url, method, **kwargs):
|
|
self.authenticate_and_fetch_endpoint_url()
|
|
# Perform the request once. If we get a 401 back then it
|
|
# might be because the auth token expired, so try to
|
|
# re-authenticate and try again. If it still fails, bail.
|
|
try:
|
|
kwargs.setdefault('headers', {})
|
|
kwargs['headers']['X-Auth-Token'] = self.auth_token
|
|
resp, body = self._cs_request(self.endpoint_url + url, method,
|
|
**kwargs)
|
|
return resp, body
|
|
except exceptions.Unauthorized:
|
|
self.authenticate()
|
|
kwargs.setdefault('headers', {})
|
|
kwargs['headers']['X-Auth-Token'] = self.auth_token
|
|
resp, body = self._cs_request(
|
|
self.endpoint_url + url, method, **kwargs)
|
|
return resp, body
|
|
|
|
def _extract_service_catalog(self, body):
|
|
"""Set the client's service catalog from the response data."""
|
|
self.service_catalog = ServiceCatalog(body)
|
|
try:
|
|
sc = self.service_catalog.get_token()
|
|
self.auth_token = sc['id']
|
|
self.auth_tenant_id = sc.get('tenant_id')
|
|
self.auth_user_id = sc.get('user_id')
|
|
except KeyError:
|
|
raise exceptions.Unauthorized()
|
|
self.endpoint_url = self.service_catalog.url_for(
|
|
attr='region', filter_value=self.region_name,
|
|
endpoint_type=self.endpoint_type)
|
|
|
|
def authenticate(self):
|
|
if self.auth_strategy != 'keystone':
|
|
raise exceptions.Unauthorized(message='unknown auth strategy')
|
|
if self.tenant_id:
|
|
body = {'auth': {'passwordCredentials':
|
|
{'username': self.username,
|
|
'password': self.password, },
|
|
'tenantId': self.tenant_id, }, }
|
|
else:
|
|
body = {'auth': {'passwordCredentials':
|
|
{'username': self.username,
|
|
'password': self.password, },
|
|
'tenantName': self.tenant_name, }, }
|
|
|
|
token_url = self.auth_url + "/tokens"
|
|
|
|
# Make sure we follow redirects when trying to reach Keystone
|
|
tmp_follow_all_redirects = self.follow_all_redirects
|
|
self.follow_all_redirects = True
|
|
try:
|
|
resp, body = self._cs_request(token_url, "POST",
|
|
body=json.dumps(body),
|
|
content_type="application/json")
|
|
finally:
|
|
self.follow_all_redirects = tmp_follow_all_redirects
|
|
status_code = self.get_status_code(resp)
|
|
if status_code != 200:
|
|
raise exceptions.Unauthorized(message=body)
|
|
if body:
|
|
try:
|
|
body = json.loads(body)
|
|
except ValueError:
|
|
pass
|
|
else:
|
|
body = None
|
|
self._extract_service_catalog(body)
|
|
|
|
def _get_endpoint_url(self):
|
|
url = self.auth_url + '/tokens/%s/endpoints' % self.auth_token
|
|
try:
|
|
resp, body = self._cs_request(url, "GET")
|
|
except exceptions.Unauthorized:
|
|
# rollback to authenticate() to handle case when neutron client
|
|
# is initialized just before the token is expired
|
|
self.authenticate()
|
|
return self.endpoint_url
|
|
|
|
body = json.loads(body)
|
|
for endpoint in body.get('endpoints', []):
|
|
if (endpoint['type'] == 'network' and
|
|
endpoint.get('region') == self.region_name):
|
|
if self.endpoint_type not in endpoint:
|
|
raise exceptions.EndpointTypeNotFound(
|
|
message=self.endpoint_type)
|
|
return endpoint[self.endpoint_type]
|
|
|
|
raise exceptions.EndpointNotFound()
|
|
|
|
def get_auth_info(self):
|
|
return {'auth_token': self.auth_token,
|
|
'auth_tenant_id': self.auth_tenant_id,
|
|
'auth_user_id': self.auth_user_id,
|
|
'endpoint_url': self.endpoint_url}
|
|
|
|
def get_status_code(self, response):
|
|
"""Returns the integer status code from the response.
|
|
|
|
Either a Webob.Response (used in testing) or httplib.Response
|
|
is returned.
|
|
"""
|
|
if hasattr(response, 'status_int'):
|
|
return response.status_int
|
|
else:
|
|
return response.status
|