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:
parent
deef71af93
commit
60d1283968
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue