Merge "Don't log sensitive auth data"

This commit is contained in:
Jenkins
2014-07-24 21:30:53 +00:00
committed by Gerrit Code Review
12 changed files with 198 additions and 62 deletions

View File

@@ -11,6 +11,7 @@
# under the License.
import abc
import logging
from oslo.config import cfg
import six
@@ -20,6 +21,8 @@ from keystoneclient.auth.identity import base
from keystoneclient import exceptions
from keystoneclient import utils
_logger = logging.getLogger(__name__)
@six.add_metaclass(abc.ABCMeta)
class Auth(base.BaseIdentityPlugin):
@@ -66,8 +69,9 @@ class Auth(base.BaseIdentityPlugin):
if self.trust_id:
params['auth']['trust_id'] = self.trust_id
_logger.debug('Making authentication request to %s', url)
resp = session.post(url, json=params, headers=headers,
authenticated=False)
authenticated=False, log=False)
try:
resp_data = resp.json()['access']

View File

@@ -105,8 +105,9 @@ class Auth(base.BaseIdentityPlugin):
elif self.trust_id:
body['auth']['scope'] = {'OS-TRUST:trust': {'id': self.trust_id}}
_logger.debug('Making authentication request to %s', self.token_url)
resp = session.post(self.token_url, json=body, headers=headers,
authenticated=False)
authenticated=False, log=False)
try:
resp_data = resp.json()['token']

View File

@@ -46,6 +46,11 @@ def getid(obj):
return obj
def filter_none(**kwargs):
"""Remove any entries from a dictionary where the value is None."""
return dict((k, v) for k, v in six.iteritems(kwargs) if v is not None)
def filter_kwargs(f):
@functools.wraps(f)
def func(*args, **kwargs):

View File

@@ -246,7 +246,7 @@ class Saml2UnscopedToken(v3.AuthConstructor):
headers={'Content-type': 'text/xml'},
data=etree.tostring(idp_saml2_authn_request),
requests_auth=(self.username, self.password),
authenticated=False)
authenticated=False, log=False)
try:
self.saml2_idp_authn_response = etree.XML(idp_response.content)

View File

@@ -130,11 +130,69 @@ class Session(object):
if user_agent is not None:
self.user_agent = user_agent
@utils.positional()
def _http_log_request(self, url, method=None, data=None,
json=None, headers=None):
if not _logger.isEnabledFor(logging.DEBUG):
# NOTE(morganfainberg): This whole debug section is expensive,
# there is no need to do the work if we're not going to emit a
# debug log.
return
string_parts = ['REQ: curl -i']
# NOTE(jamielennox): None means let requests do its default validation
# so we need to actually check that this is False.
if self.verify is False:
string_parts.append('--insecure')
if method:
string_parts.extend(['-X', method])
string_parts.append(url)
if headers:
for header in six.iteritems(headers):
string_parts.append('-H "%s: %s"' % header)
if json:
data = jsonutils.dumps(json)
if data:
string_parts.append("-d '%s'" % data)
_logger.debug(' '.join(string_parts))
@utils.positional()
def _http_log_response(self, response=None, json=None,
status_code=None, headers=None, text=None):
if not _logger.isEnabledFor(logging.DEBUG):
return
if response:
if not status_code:
status_code = response.status_code
if not headers:
headers = response.headers
if not text:
text = response.text
if json:
text = jsonutils.dumps(json)
string_parts = ['RESP:']
if status_code:
string_parts.append('[%s]' % status_code)
if headers:
string_parts.append('%s' % headers)
if text:
string_parts.append('\nRESP BODY: %s\n' % text)
_logger.debug(' '.join(string_parts))
@utils.positional(enforcement=utils.positional.WARN)
def request(self, url, method, json=None, original_ip=None,
user_agent=None, redirect=None, authenticated=None,
endpoint_filter=None, auth=None, requests_auth=None,
raise_exc=True, allow_reauth=True, **kwargs):
raise_exc=True, allow_reauth=True, log=True, **kwargs):
"""Send an HTTP request with the specified characteristics.
Wrapper around `requests.Session.request` to handle tasks such as
@@ -183,6 +241,8 @@ class Session(object):
:param bool allow_reauth: Allow fetching a new token and retrying the
request on receiving a 401 Unauthorized
response. (optional, default True)
:param bool log: If True then log the request and response data to the
debug log. (optional, default True)
:param kwargs: any other parameter that can be passed to
requests.Session.request (such as `headers`). Except:
'data' will be overwritten by the data in 'json' param.
@@ -250,28 +310,10 @@ class Session(object):
if requests_auth:
kwargs['auth'] = requests_auth
string_parts = ['curl -i']
# NOTE(jamielennox): None means let requests do its default validation
# so we need to actually check that this is False.
if self.verify is False:
string_parts.append('--insecure')
if method:
string_parts.extend(['-X', method])
string_parts.append(url)
if headers:
for header in six.iteritems(headers):
string_parts.append('-H "%s: %s"' % header)
try:
string_parts.append("-d '%s'" % kwargs['data'])
except KeyError:
pass
_logger.debug('REQ: %s', ' '.join(string_parts))
if log:
self._http_log_request(url, method=method,
data=kwargs.get('data'),
headers=headers)
# Force disable requests redirect handling. We will manage this below.
kwargs['allow_redirects'] = False
@@ -279,7 +321,7 @@ class Session(object):
if redirect is None:
redirect = self.redirect
resp = self._send_request(url, method, redirect, **kwargs)
resp = self._send_request(url, method, redirect, log, **kwargs)
# handle getting a 401 Unauthorized response by invalidating the plugin
# and then retrying the request. This is only tried once.
@@ -288,7 +330,8 @@ class Session(object):
token = self.get_token(auth)
if token:
headers['X-Auth-Token'] = token
resp = self._send_request(url, method, redirect, **kwargs)
resp = self._send_request(url, method, redirect, log,
**kwargs)
if raise_exc and resp.status_code >= 400:
_logger.debug('Request returned failure status: %s',
@@ -297,7 +340,7 @@ class Session(object):
return resp
def _send_request(self, url, method, redirect, **kwargs):
def _send_request(self, url, method, redirect, log, **kwargs):
# NOTE(jamielennox): We handle redirection manually because the
# requests lib follows some browser patterns where it will redirect
# POSTs as GETs for certain statuses which is not want we want for an
@@ -315,8 +358,8 @@ class Session(object):
msg = 'Unable to establish connection to %s' % url
raise exceptions.ConnectionRefused(msg)
_logger.debug('RESP: [%s] %s\nRESP BODY: %s\n',
resp.status_code, resp.headers, resp.text)
if log:
self._http_log_response(response=resp)
if resp.status_code in self.REDIRECT_STATUSES:
# be careful here in python True == 1 and False == 0
@@ -335,7 +378,7 @@ class Session(object):
_logger.warn("Failed to redirect request to %s as new "
"location was not provided.", resp.url)
else:
new_resp = self._send_request(location, method, redirect,
new_resp = self._send_request(location, method, redirect, log,
**kwargs)
if not isinstance(new_resp.history, list):

View File

@@ -11,6 +11,7 @@
# under the License.
import copy
import uuid
import httpretty
from six.moves import urllib
@@ -255,3 +256,14 @@ class V2IdentityPlugin(utils.TestCase):
self.assertEqual('token1', s.get_token())
a.invalidate()
self.assertEqual('token2', s.get_token())
@httpretty.activate
def test_doesnt_log_password(self):
self.stub_auth(json=self.TEST_RESPONSE_DICT)
password = uuid.uuid4().hex
a = v2.Password(self.TEST_URL, username=self.TEST_USER,
password=password)
s = session.Session(auth=a)
self.assertEqual(self.TEST_TOKEN, s.get_token())
self.assertNotIn(password, self.logger.output)

View File

@@ -11,6 +11,7 @@
# under the License.
import copy
import uuid
import httpretty
from six.moves import urllib
@@ -408,3 +409,15 @@ class V3IdentityPlugin(utils.TestCase):
self.assertEqual('token1', s.get_token())
a.invalidate()
self.assertEqual('token2', s.get_token())
@httpretty.activate
def test_doesnt_log_password(self):
self.stub_auth(json=self.TEST_RESPONSE_DICT)
password = uuid.uuid4().hex
a = v3.Password(self.TEST_URL, username=self.TEST_USER,
password=password)
s = session.Session(a)
self.assertEqual(self.TEST_TOKEN, s.get_token())
self.assertNotIn(password, self.logger.output)

View File

@@ -47,10 +47,11 @@ class UserTests(utils.TestCase):
def test_create(self):
tenant_id = uuid.uuid4().hex
user_id = uuid.uuid4().hex
password = uuid.uuid4().hex
req_body = {
"user": {
"name": "gabriel",
"password": "test",
"password": password,
"tenantId": tenant_id,
"email": "test@example.com",
"enabled": True,
@@ -63,7 +64,7 @@ class UserTests(utils.TestCase):
"enabled": True,
"tenantId": tenant_id,
"id": user_id,
"password": "test",
"password": password,
"email": "test@example.com",
}
}
@@ -80,6 +81,7 @@ class UserTests(utils.TestCase):
self.assertEqual(user.name, "gabriel")
self.assertEqual(user.email, "test@example.com")
self.assertRequestBodyIs(json=req_body)
self.assertNotIn(password, self.logger.output)
@httpretty.activate
def test_create_user_without_email(self):
@@ -210,10 +212,11 @@ class UserTests(utils.TestCase):
"name": "gabriel",
}
}
password = uuid.uuid4().hex
req_2 = {
"user": {
"id": self.DEMO_USER_ID,
"password": "swordfish",
"password": password,
}
}
tenant_id = uuid.uuid4().hex
@@ -245,18 +248,22 @@ class UserTests(utils.TestCase):
name='gabriel',
email='gabriel@example.com')
self.assertRequestBodyIs(json=req_1)
self.client.users.update_password(self.DEMO_USER_ID, 'swordfish')
self.client.users.update_password(self.DEMO_USER_ID, password)
self.assertRequestBodyIs(json=req_2)
self.client.users.update_tenant(self.DEMO_USER_ID, tenant_id)
self.assertRequestBodyIs(json=req_3)
self.client.users.update_enabled(self.DEMO_USER_ID, False)
self.assertRequestBodyIs(json=req_4)
self.assertNotIn(password, self.logger.output)
@httpretty.activate
def test_update_own_password(self):
old_password = uuid.uuid4().hex
new_password = uuid.uuid4().hex
req_body = {
'user': {
'password': 'ABCD', 'original_password': 'DCBA'
'password': new_password,
'original_password': old_password
}
}
resp_body = {
@@ -267,8 +274,10 @@ class UserTests(utils.TestCase):
json=resp_body)
self.client.user_id = user_id
self.client.users.update_own_password('DCBA', 'ABCD')
self.client.users.update_own_password(old_password, new_password)
self.assertRequestBodyIs(json=req_body)
self.assertNotIn(old_password, self.logger.output)
self.assertNotIn(new_password, self.logger.output)
@httpretty.activate
def test_user_role_listing(self):

View File

@@ -96,6 +96,25 @@ class UserTests(utils.TestCase, utils.CrudTests):
user=ref['id'],
group=None)
@httpretty.activate
def test_create_doesnt_log_password(self):
password = uuid.uuid4().hex
ref = self.new_ref()
self.stub_entity(httpretty.POST, [self.collection_key],
status=201, entity=ref)
req_ref = ref.copy()
req_ref.pop('id')
param_ref = req_ref.copy()
param_ref['password'] = password
params = utils.parameterize(param_ref)
self.manager.create(**params)
self.assertNotIn(password, self.logger.output)
@httpretty.activate
def test_create_with_project(self):
# Can create a user with the deprecated project option rather than
@@ -148,6 +167,26 @@ class UserTests(utils.TestCase, utils.CrudTests):
'Expected different %s' % attr)
self.assertEntityRequestBodyIs(req_ref)
@httpretty.activate
def test_update_doesnt_log_password(self):
password = uuid.uuid4().hex
ref = self.new_ref()
req_ref = ref.copy()
req_ref.pop('id')
param_ref = req_ref.copy()
self.stub_entity(httpretty.PATCH,
[self.collection_key, ref['id']],
status=200, entity=ref)
param_ref['password'] = password
params = utils.parameterize(param_ref)
self.manager.update(ref['id'], **params)
self.assertNotIn(password, self.logger.output)
@httpretty.activate
def test_update_with_project(self):
# Can update a user with the deprecated project option rather than
@@ -217,6 +256,8 @@ class UserTests(utils.TestCase, utils.CrudTests):
self.assertEqual('/v3/users/test/password',
httpretty.last_request().path)
self.assertRequestBodyIs(json=exp_req_body)
self.assertNotIn(old_password, self.logger.output)
self.assertNotIn(new_password, self.logger.output)
def test_update_password_with_bad_inputs(self):
old_password = uuid.uuid4().hex

View File

@@ -53,7 +53,7 @@ class TokenManager(base.Manager):
reset = 1
self.api.management_url = self.api.auth_url
token_ref = self._create('/tokens', params, "access",
return_raw=return_raw)
return_raw=return_raw, log=False)
if reset:
self.api.management_url = None
return token_ref

View File

@@ -68,7 +68,7 @@ class UserManager(base.ManagerWithFind):
"password": password}}
return self._update("/users/%s/OS-KSADM/password" % base.getid(user),
params, "user")
params, "user", log=False)
def update_own_password(self, origpasswd, passwd):
"""Update password."""
@@ -78,7 +78,8 @@ class UserManager(base.ManagerWithFind):
return self._update("/OS-KSCRUD/users/%s" % self.api.user_id, params,
response_key="access",
method="PATCH",
management=False)
management=False,
log=False)
def update_tenant(self, user, tenant):
"""Update default tenant."""
@@ -98,7 +99,7 @@ class UserManager(base.ManagerWithFind):
"tenantId": tenant_id,
"email": email,
"enabled": enabled}}
return self._create('/users', params, "user")
return self._create('/users', params, "user", log=not bool(password))
def delete(self, user):
"""Delete a user."""

View File

@@ -61,15 +61,17 @@ class UserManager(base.CrudManager):
LOG.warning("The project argument is deprecated, "
"use default_project instead.")
default_project_id = base.getid(default_project) or base.getid(project)
return super(UserManager, self).create(
name=name,
domain_id=base.getid(domain),
default_project_id=default_project_id,
password=password,
email=email,
description=description,
enabled=enabled,
**kwargs)
user_data = base.filter_none(name=name,
domain_id=base.getid(domain),
default_project_id=default_project_id,
password=password,
email=email,
description=description,
enabled=enabled,
**kwargs)
return self._create('/users', {'user': user_data}, 'user',
log=not bool(password))
@utils.positional(enforcement=utils.positional.WARN)
def list(self, project=None, domain=None, group=None, default_project=None,
@@ -125,16 +127,20 @@ class UserManager(base.CrudManager):
LOG.warning("The project argument is deprecated, "
"use default_project instead.")
default_project_id = base.getid(default_project) or base.getid(project)
return super(UserManager, self).update(
user_id=base.getid(user),
name=name,
domain_id=base.getid(domain),
default_project_id=default_project_id,
password=password,
email=email,
description=description,
enabled=enabled,
**kwargs)
user_data = base.filter_none(name=name,
domain_id=base.getid(domain),
default_project_id=default_project_id,
password=password,
email=email,
description=description,
enabled=enabled,
**kwargs)
return self._update('/users/%s' % base.getid(user),
{'user': user_data},
'user',
method='PATCH',
log=False)
def update_password(self, old_password, new_password):
"""Update the password for the user the token belongs to."""
@@ -151,7 +157,8 @@ class UserManager(base.CrudManager):
base_url = '/users/%s/password' % self.api.user_id
return self._update(base_url, params, method='POST', management=False)
return self._update(base_url, params, method='POST', management=False,
log=False)
def add_to_group(self, user, group):
self._require_user_and_group(user, group)