Don't log sensitive auth data

This code change redacts the password in keystone request, and
also redact the token text in keystone response. The code still
makes REST call by itelf, instead of calling keystone client.

Closes-Bug: 1327019

Change-Id: Ib9c0610c1ef351a127364478721cf961c2a30125
This commit is contained in:
Qin Zhao 2014-07-15 18:25:57 +08:00
parent deef71af93
commit 60d1283968
2 changed files with 87 additions and 16 deletions

View File

@ -20,6 +20,7 @@
OpenStack Client interface. Handles the REST calls and responses.
"""
import copy
import errno
import functools
import glob
@ -44,8 +45,6 @@ from novaclient.openstack.common import network_utils
from novaclient import service_catalog
from novaclient import utils
SENSITIVE_HEADERS = ('X-Auth-Token',)
class _ClientConnectionPool(object):
@ -318,15 +317,41 @@ class HTTPClient(object):
def reset_timings(self):
self.times = []
def safe_header(self, name, value):
if name in SENSITIVE_HEADERS:
# because in python3 byte string handling is ... ug
v = value.encode('utf-8')
h = hashlib.sha1(v)
d = h.hexdigest()
return name, "{SHA1}%s" % d
else:
return name, value
def _redact(self, target, path, text=None):
"""Replace the value of a key in `target`.
The key can be at the top level by specifying a list with a single
key as the path. Nested dictionaries are also supported by passing a
list of keys to be navigated to find the one that should be replaced.
In this case the last one is the one that will be replaced.
:param dict target: the dictionary that may have a key to be redacted;
modified in place
:param list path: a list representing the nested structure in `target`
that should be redacted; modified in place
:param string text: optional text to use as a replacement for the
redacted key. if text is not specified, the
default text will be sha1 hash of the value being
redacted
"""
key = path.pop()
# move to the most nested dict
for p in path:
try:
target = target[p]
except KeyError:
return
if key in target:
if text:
target[key] = text
else:
# because in python3 byte string handling is ... ug
value = target[key].encode('utf-8')
sha1sum = hashlib.sha1(value)
target[key] = "{SHA1}%s" % sha1sum.hexdigest()
def http_log_req(self, method, url, kwargs):
if not self.http_log_debug:
@ -340,24 +365,38 @@ class HTTPClient(object):
string_parts.append(" '%s'" % url)
string_parts.append(' -X %s' % method)
headers = copy.deepcopy(kwargs['headers'])
self._redact(headers, ['X-Auth-Token'])
# because dict ordering changes from 2 to 3
keys = sorted(kwargs['headers'].keys())
keys = sorted(headers.keys())
for name in keys:
value = kwargs['headers'][name]
header = ' -H "%s: %s"' % self.safe_header(name, value)
value = headers[name]
header = ' -H "%s: %s"' % (name, value)
string_parts.append(header)
if 'data' in kwargs:
string_parts.append(" -d '%s'" % (kwargs['data']))
data = json.loads(kwargs['data'])
self._redact(data, ['auth', 'passwordCredentials', 'password'])
string_parts.append(" -d '%s'" % json.dumps(data))
self._logger.debug("REQ: %s" % "".join(string_parts))
def http_log_resp(self, resp):
if not self.http_log_debug:
return
if resp.text and resp.status_code != 400:
try:
body = json.loads(resp.text)
self._redact(body, ['access', 'token', 'id'])
except ValueError:
body = None
else:
body = None
self._logger.debug("RESP: [%(status)s] %(headers)s\nRESP BODY: "
"%(text)s\n", {'status': resp.status_code,
'headers': resp.headers,
'text': resp.text})
'text': json.dumps(body)})
def open_session(self):
if not self._connection_pool:

View File

@ -343,6 +343,9 @@ class ClientTest(utils.TestCase):
{'X-Foo': 'bar',
'X-Auth-Token': 'totally_bogus'}
})
cs.http_log_req('GET', '/foo', {'headers': {},
'data': '{"auth": {"passwordCredentials": '
'{"password": "zhaoqin"}}}'})
output = self.logger.output.split('\n')
@ -356,3 +359,32 @@ class ClientTest(utils.TestCase):
'"X-Auth-Token: {SHA1}b42162b6ffdbd7c3c37b7c95b7ba9f51dda0236d"'
' -H "X-Foo: bar"',
output)
self.assertIn(
"REQ: curl -i '/foo' -X GET -d "
'\'{"auth": {"passwordCredentials": {"password":'
' "{SHA1}4fc49c6a671ce889078ff6b250f7066cf6d2ada2"}}}\'',
output)
def test_log_resp(self):
self.logger = self.useFixture(
fixtures.FakeLogger(
format="%(message)s",
level=logging.DEBUG,
nuke_handlers=True
)
)
cs = novaclient.client.HTTPClient("user", None, "",
connection_pool=True)
cs.http_log_debug = True
text = ('{"access": {"token": {"id": "zhaoqin"}}}')
resp = utils.TestResponse({'status_code': 200, 'headers': {},
'text': text})
cs.http_log_resp(resp)
output = self.logger.output.split('\n')
self.assertIn('RESP: [200] {}', output)
self.assertIn('RESP BODY: {"access": {"token": {"id":'
' "{SHA1}4fc49c6a671ce889078ff6b250f7066cf6d2ada2"}}}',
output)