Memoize calls to bcrypt.checkpw
This intentionally high CPU overhead function is called for every API and JSON-RPC request when Basic HTTP authentication is enabled. With the recent indirection enablement this is causing a performance regression for Metal3 due to the extra JSON-RPC calls. This change would improve the performance of all branches of Metal3 if backported. Change-Id: I2740035d2882aacddca9c541362d6e533140650f Closes-Bug: #2121105 Signed-off-by: Steve Baker <sbaker@redhat.com>
This commit is contained in:
@@ -15,6 +15,7 @@
|
|||||||
|
|
||||||
import base64
|
import base64
|
||||||
import binascii
|
import binascii
|
||||||
|
import functools
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import bcrypt
|
import bcrypt
|
||||||
@@ -91,6 +92,16 @@ def authenticate(auth_file, username, password):
|
|||||||
unauthorized()
|
unauthorized()
|
||||||
|
|
||||||
|
|
||||||
|
@functools.lru_cache(maxsize=256)
|
||||||
|
def _checkpw(password, hashed):
|
||||||
|
"""Wrapped bcrypt.checkpw for caching
|
||||||
|
|
||||||
|
Keep an in-memory cache of bcrypt.checkpw responses to avoid the
|
||||||
|
high CPU cost of repeatedly checking the same values
|
||||||
|
"""
|
||||||
|
return bcrypt.checkpw(password, hashed)
|
||||||
|
|
||||||
|
|
||||||
def auth_entry(entry, password):
|
def auth_entry(entry, password):
|
||||||
"""Compare a password with a single user auth file entry
|
"""Compare a password with a single user auth file entry
|
||||||
|
|
||||||
@@ -102,7 +113,7 @@ def auth_entry(entry, password):
|
|||||||
"""
|
"""
|
||||||
username, encrypted = parse_entry(entry)
|
username, encrypted = parse_entry(entry)
|
||||||
|
|
||||||
if not bcrypt.checkpw(password, encrypted):
|
if not _checkpw(password, encrypted):
|
||||||
LOG.info('Password for %s does not match', username)
|
LOG.info('Password for %s does not match', username)
|
||||||
unauthorized()
|
unauthorized()
|
||||||
|
|
||||||
|
@@ -15,15 +15,16 @@
|
|||||||
Tests to assert that various incorporated middleware works as expected.
|
Tests to assert that various incorporated middleware works as expected.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import bcrypt
|
||||||
from http import client as http_client
|
from http import client as http_client
|
||||||
import os
|
import os
|
||||||
import tempfile
|
import tempfile
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
import oslo_middleware.cors as cors_middleware
|
import oslo_middleware.cors as cors_middleware
|
||||||
|
|
||||||
from ironic.tests.unit.api import base
|
from ironic.tests.unit.api import base
|
||||||
from ironic.tests.unit.api import utils
|
|
||||||
from ironic.tests.unit.db import utils as db_utils
|
from ironic.tests.unit.db import utils as db_utils
|
||||||
|
|
||||||
|
|
||||||
@@ -132,7 +133,7 @@ class TestBasicAuthMiddleware(base.BaseApiTest):
|
|||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(TestBasicAuthMiddleware, self).setUp()
|
super(TestBasicAuthMiddleware, self).setUp()
|
||||||
self.environ = {'fake.cache': utils.FakeMemcache()}
|
self.environ = {}
|
||||||
self.fake_db_node = db_utils.get_test_node(chassis_id=None)
|
self.fake_db_node = db_utils.get_test_node(chassis_id=None)
|
||||||
|
|
||||||
def test_not_authenticated(self):
|
def test_not_authenticated(self):
|
||||||
@@ -148,6 +149,20 @@ class TestBasicAuthMiddleware(base.BaseApiTest):
|
|||||||
response = self.get_json('/chassis', headers=auth_header)
|
response = self.get_json('/chassis', headers=auth_header)
|
||||||
self.assertEqual({'chassis': []}, response)
|
self.assertEqual({'chassis': []}, response)
|
||||||
|
|
||||||
|
@mock.patch.object(bcrypt, 'checkpw', autospec=True)
|
||||||
|
def test_authenticated_cached(self, mock_checkpw):
|
||||||
|
def checkpw(password, hashed):
|
||||||
|
return password == b'myPassword'
|
||||||
|
mock_checkpw.side_effect = checkpw
|
||||||
|
|
||||||
|
auth_header = {'Authorization': 'Basic bXlOYW1lOm15UGFzc3dvcmQ='}
|
||||||
|
for i in range(10):
|
||||||
|
response = self.get_json('/chassis', headers=auth_header)
|
||||||
|
self.assertEqual({'chassis': []}, response)
|
||||||
|
# call count will be zero or one, depending on already cached
|
||||||
|
# results from other tests
|
||||||
|
self.assertLessEqual(mock_checkpw.call_count, 1)
|
||||||
|
|
||||||
def test_public_unauthenticated(self):
|
def test_public_unauthenticated(self):
|
||||||
response = self.get_json('/')
|
response = self.get_json('/')
|
||||||
self.assertEqual('v1', response['id'])
|
self.assertEqual('v1', response['id'])
|
||||||
|
7
releasenotes/notes/bcrypt_cache-d78775ff02f2d970.yaml
Normal file
7
releasenotes/notes/bcrypt_cache-d78775ff02f2d970.yaml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
fixes:
|
||||||
|
- |
|
||||||
|
Performance of Basic HTTP authentication has been improved by keeping a
|
||||||
|
memory cache of bcrypt password checks. This improves the performance of
|
||||||
|
Ironic conductor with JSON-RPC, and API access when using Basic HTTP
|
||||||
|
authentication.
|
Reference in New Issue
Block a user