Enable Basic HTTP authentication middleware

When the config option ``auth_strategy`` is set to ``http_basic`` then
non-public API calls require a valid HTTP Basic authentication header
to be set. The config option ``http_basic_auth_user_file`` defaults to
``/etc/ironic/htpasswd`` and points to a file which supports the
Apache htpasswd syntax[1]. This file is read for every request, so no
service restart is required when changes are made.

The only password digest supported is bcrypt, and the ``bcrypt``
python library is used for password checks since it supports ``$2y$``
prefixed bcrypt passwords as generated by the Apache htpasswd utility.

To try HTTP basic authentication, the following can be done:

* Set ``/etc/ironic/ironic.conf`` ``DEFAULT`` ``auth_strategy`` to ``http_basic``
* Populate the htpasswd file with entries, for example:
  ``htpasswd -nbB myName myPassword >> /etc/ironic/htpasswd``
* Make basic authenticated HTTP requests, for example:
  ``curl --user myName:myPassword http://localhost:6385/v1/drivers``

[1] https://httpd.apache.org/docs/current/misc/password_encryptions.html

Change-Id: I7b89155d8bbd2f48e186c12adea9d6932cd0bfe2
Story: 2007656
Task: 39825
Depends-On: https://review.opendev.org/729070
This commit is contained in:
Steve Baker 2020-05-19 10:50:28 +12:00
parent 3daffe07a8
commit c6112b01c3
9 changed files with 93 additions and 9 deletions

View File

@ -15,6 +15,7 @@
# License for the specific language governing permissions and limitations
# under the License.
from ironic_lib import auth_basic
import keystonemiddleware.audit as audit_middleware
from keystonemiddleware import auth_token
from oslo_config import cfg
@ -98,11 +99,18 @@ def setup_app(pecan_config=None, extra_hooks=None):
reason=e
)
auth_middleware = None
if CONF.auth_strategy == "keystone":
auth_middleware = auth_token.AuthProtocol(
app, {"oslo_config_config": cfg.CONF})
elif CONF.auth_strategy == "http_basic":
auth_middleware = auth_basic.BasicAuthMiddleware(
app, cfg.CONF.http_basic_auth_user_file)
if auth_middleware:
app = auth_public_routes.AuthPublicRoutes(
app,
auth=auth_token.AuthProtocol(
app, {"oslo_config_config": cfg.CONF}),
auth=auth_middleware,
public_api_routes=pecan_config.app.acl_public_routes)
if CONF.profiler.enabled:

View File

@ -97,7 +97,7 @@ class ContextHook(hooks.PecanHook):
ctx = context.RequestContext.from_environ(state.request.environ,
is_public_api=is_public_api)
# Do not pass any token with context for noauth mode
if cfg.CONF.auth_strategy == 'noauth':
if cfg.CONF.auth_strategy != 'keystone':
ctx.auth_token = None
creds = ctx.to_policy_values()

View File

@ -633,9 +633,9 @@ def authorize(rule, target, creds, *args, **kwargs):
Checks authorization of a rule against the target and credentials, and
raises an exception if the rule is not defined.
Always returns true if CONF.auth_strategy == noauth.
Always returns true if CONF.auth_strategy is not keystone.
"""
if CONF.auth_strategy == 'noauth':
if CONF.auth_strategy != 'keystone':
return True
enforcer = get_enforcer()
try:

View File

@ -58,10 +58,15 @@ api_opts = [
default='keystone',
choices=[('noauth', _('no authentication')),
('keystone', _('use the Identity service for '
'authentication'))],
'authentication')),
('http_basic', _('HTTP basic authentication'))],
help=_('Authentication strategy used by ironic-api. "noauth" should '
'not be used in a production environment because all '
'authentication will be disabled.')),
cfg.StrOpt('http_basic_auth_user_file',
default='/etc/ironic/htpasswd',
help=_('Path to Apache format user authentication file used '
'when auth_strategy=http_basic')),
cfg.BoolOpt('debug_tracebacks_in_api',
default=False,
help=_('Return server tracebacks in the API response for any '

View File

@ -45,7 +45,7 @@ _IRONIC_MANAGES_BOOT = 'inspector_manage_boot'
def _get_inspector_session(**kwargs):
global _INSPECTOR_SESSION
if not _INSPECTOR_SESSION:
if CONF.auth_strategy == 'noauth':
if CONF.auth_strategy != 'keystone':
# NOTE(dtantsur): using set_default instead of set_override because
# the native keystoneauth option must have priority.
CONF.set_default('auth_type', 'none', group='inspector')

View File

@ -16,11 +16,15 @@ Tests to assert that various incorporated middleware works as expected.
"""
from http import client as http_client
import os
import tempfile
from oslo_config import cfg
import oslo_middleware.cors as cors_middleware
from ironic.tests.unit.api import base
from ironic.tests.unit.api import utils
from ironic.tests.unit.db import utils as db_utils
class TestCORSMiddleware(base.BaseApiTest):
@ -112,3 +116,38 @@ class TestCORSMiddleware(base.BaseApiTest):
self.assertEqual(
self._response_string(http_client.OK), response.status)
self.assertNotIn('Access-Control-Allow-Origin', response.headers)
class TestBasicAuthMiddleware(base.BaseApiTest):
def _make_app(self):
with tempfile.NamedTemporaryFile(mode='w', delete=False) as f:
f.write('myName:$2y$05$lE3eGtyj41jZwrzS87KTqe6.'
'JETVCWBkc32C63UP2aYrGoYOEpbJm\n\n\n')
cfg.CONF.set_override('http_basic_auth_user_file', f.name)
self.addCleanup(os.remove, cfg.CONF.http_basic_auth_user_file)
cfg.CONF.set_override('auth_strategy', 'http_basic')
return super(TestBasicAuthMiddleware, self)._make_app()
def setUp(self):
super(TestBasicAuthMiddleware, self).setUp()
self.environ = {'fake.cache': utils.FakeMemcache()}
self.fake_db_node = db_utils.get_test_node(chassis_id=None)
def test_not_authenticated(self):
response = self.get_json('/chassis', expect_errors=True)
self.assertEqual(http_client.UNAUTHORIZED, response.status_int)
self.assertEqual(
'Basic realm="Baremetal API"',
response.headers['WWW-Authenticate']
)
def test_authenticated(self):
auth_header = {'Authorization': 'Basic bXlOYW1lOm15UGFzc3dvcmQ='}
response = self.get_json('/chassis', headers=auth_header)
self.assertEqual({'chassis': []}, response)
def test_public_unauthenticated(self):
response = self.get_json('/')
self.assertEqual('v1', response['id'])

View File

@ -35,7 +35,7 @@ greenlet==0.4.15
hacking==3.0.0
ifaddr==0.1.6
importlib-metadata==1.6.0
ironic-lib==2.17.1
ironic-lib==4.3.0
iso8601==0.1.11
Jinja2==2.10
jmespath==0.9.5

View File

@ -0,0 +1,32 @@
---
features:
- |
Enable Basic HTTP authentication middleware.
Having noauth as the only option for standalone ironic causes constraints
on how the API is exposed on the network. Having some kind of
authentication layer behind a TLS deployment eases these constraints.
When the config option ``auth_strategy`` is set to ``http_basic`` then
non-public API calls require a valid HTTP Basic authentication header to
be set. The config option ``http_basic_auth_user_file`` defaults to
``/etc/ironic/htpasswd`` and points to a file which supports the Apache
htpasswd syntax[1]. This file is read for every request, so no service
restart is required when changes are made.
Like the ``noauth`` auth strategy, the ``http_basic`` auth strategy is
intended for standalone deployments of ironic, and integration with other
OpenStack services cannot depend on a service catalog.
The only password digest supported is bcrypt, and the ``bcrypt`` python
library is used for password checks since it supports ``$2y$`` prefixed
bcrypt passwords as generated by the Apache htpasswd utility.
To try HTTP basic authentication, the following can be done:
* Set ``/etc/ironic/ironic.conf`` ``DEFAULT`` ``auth_strategy`` to
* ``http_basic`` Populate the htpasswd file with entries, for example:
``htpasswd -nbB myName myPassword >> /etc/ironic/htpassw
* Make basic authenticated HTTP requests, for example:
``curl --user myName:myPassword http://localhost:6385/v1/drivers``
[1] https://httpd.apache.org/docs/current/misc/password_encryptions.html

View File

@ -11,7 +11,7 @@ python-cinderclient!=4.0.0,>=3.3.0 # Apache-2.0
python-neutronclient>=6.7.0 # Apache-2.0
python-glanceclient>=2.8.0 # Apache-2.0
keystoneauth1>=3.18.0 # Apache-2.0
ironic-lib>=2.17.1 # Apache-2.0
ironic-lib>=4.3.0 # Apache-2.0
python-swiftclient>=3.2.0 # Apache-2.0
pytz>=2013.6 # MIT
stevedore>=1.20.0 # Apache-2.0