Support HTTP basic auth

Change-Id: I58d5e43c33f5ebce4fc7f120917839d8dd28c56b
This commit is contained in:
Dmitry Tantsur 2021-08-05 14:15:58 +02:00
parent 00fcd2f4f7
commit 7998eeca08
5 changed files with 83 additions and 1 deletions

View File

@ -13,6 +13,9 @@ SUSHY_EMULATOR_SSL_CERT = None
# If SSL certificate is being served, this is its RSA private key
SUSHY_EMULATOR_SSL_KEY = None
# If authentication is desired, set this to an htpasswd file.
SUSHY_EMULATOR_AUTH_FILE = None
# The OpenStack cloud ID to use. This option enables OpenStack driver.
SUSHY_EMULATOR_OS_CLOUD = None

View File

@ -0,0 +1,5 @@
---
features:
- |
Supports HTTP basic authentication of Redfish endpoints. Set the new
``SUSHY_EMULATOR_AUTH_FILE`` variable to the path of an htpasswd file.

View File

@ -6,3 +6,4 @@ pbr!=2.1.0,>=2.0.0 # Apache-2.0
Flask>=1.0.2 # BSD
requests>=2.14.2 # Apache-2.0
tenacity>=6.2.0 # Apache-2.0
ironic-lib>=4.6.1 # Apache-2.0

View File

@ -22,6 +22,7 @@ import ssl
import sys
import flask
from ironic_lib import auth_basic
from werkzeug import exceptions as wz_exc
from sushy_tools.emulator import memoize
@ -38,15 +39,54 @@ from sushy_tools import error
from sushy_tools.error import FishyError
def _render_error(message):
return {
"error": {
"code": "Base.1.0.GeneralError",
"message": message,
"@Message.ExtendedInfo": [
{
"@odata.type": ("/redfish/v1/$metadata"
"#Message.1.0.0.Message"),
"MessageId": "Base.1.0.GeneralError"
}
]
}
}
class RedfishAuthMiddleware(auth_basic.BasicAuthMiddleware):
_EXCLUDE_PATHS = frozenset(['', 'redfish', 'redfish/v1'])
def __call__(self, env, start_response):
path = env.get('PATH_INFO', '')
if path.strip('/') in self._EXCLUDE_PATHS:
return self.app(env, start_response)
else:
return super().__call__(env, start_response)
def format_exception(self, e):
response = super().format_exception(e)
response.json_body = _render_error(str(e))
return response
class Application(flask.Flask):
def __init__(self):
def __init__(self, extra_config=None):
super().__init__(__name__)
# Turn off strict_slashes on all routes
self.url_map.strict_slashes = False
config_file = os.environ.get('SUSHY_EMULATOR_CONFIG')
if config_file:
self.config.from_pyfile(config_file)
if extra_config:
self.config.update(extra_config)
auth_file = self.config.get("SUSHY_EMULATOR_AUTH_FILE")
if auth_file:
self.wsgi_app = RedfishAuthMiddleware(self.wsgi_app, auth_file)
@property
@memoize.memoize()

View File

@ -9,6 +9,8 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import tempfile
from unittest import mock
from oslotest import base
@ -51,6 +53,37 @@ class CommonTestCase(EmulatorTestCase):
self.assertEqual('RedvirtService', response.json['Id'])
TEST_PASSWD = \
b"admin:$2y$05$mYl8KMwM94l4LR/sw1teIeA6P2u8gfX16e8wvT7NmGgAM5r9jgLl."
class AuthenticatedTestCase(base.BaseTestCase):
def setUp(self):
super().setUp()
self.auth_file = tempfile.NamedTemporaryFile()
self.auth_file.write(TEST_PASSWD)
self.auth_file.flush()
self.addCleanup(self.auth_file.close)
app = main.Application({
'SUSHY_EMULATOR_AUTH_FILE': self.auth_file.name})
self.app = app.test_client()
def test_root_resource(self):
response = self.app.get('/redfish/v1/')
# 404 because this application does not have any routes
self.assertEqual(404, response.status_code, response.data)
def test_authenticated_resource(self):
response = self.app.get('/redfish/v1/Systems/',
auth=('admin', 'password'))
self.assertEqual(404, response.status_code, response.data)
def test_authentication_failed(self):
response = self.app.get('/redfish/v1/Systems/')
self.assertEqual(401, response.status_code, response.data)
class ChassisTestCase(EmulatorTestCase):
@patch_resource('chassis')