From 7998eeca08f6128a8d1336e470eb03dbd4ecae4b Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Thu, 5 Aug 2021 14:15:58 +0200 Subject: [PATCH] Support HTTP basic auth Change-Id: I58d5e43c33f5ebce4fc7f120917839d8dd28c56b --- doc/source/admin/emulator.conf | 3 ++ releasenotes/notes/auth-044dab149ab0c03f.yaml | 5 +++ requirements.txt | 1 + sushy_tools/emulator/main.py | 42 ++++++++++++++++++- sushy_tools/tests/unit/emulator/test_main.py | 33 +++++++++++++++ 5 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/auth-044dab149ab0c03f.yaml diff --git a/doc/source/admin/emulator.conf b/doc/source/admin/emulator.conf index f645826e..8e0db8cd 100644 --- a/doc/source/admin/emulator.conf +++ b/doc/source/admin/emulator.conf @@ -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 diff --git a/releasenotes/notes/auth-044dab149ab0c03f.yaml b/releasenotes/notes/auth-044dab149ab0c03f.yaml new file mode 100644 index 00000000..b6504481 --- /dev/null +++ b/releasenotes/notes/auth-044dab149ab0c03f.yaml @@ -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. diff --git a/requirements.txt b/requirements.txt index 32a9b968..834ac647 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 diff --git a/sushy_tools/emulator/main.py b/sushy_tools/emulator/main.py index 8a6eaee5..0b829ebd 100755 --- a/sushy_tools/emulator/main.py +++ b/sushy_tools/emulator/main.py @@ -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() diff --git a/sushy_tools/tests/unit/emulator/test_main.py b/sushy_tools/tests/unit/emulator/test_main.py index f8c744d7..4d8663d4 100644 --- a/sushy_tools/tests/unit/emulator/test_main.py +++ b/sushy_tools/tests/unit/emulator/test_main.py @@ -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')