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:
parent
3daffe07a8
commit
c6112b01c3
@ -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:
|
||||
|
@ -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()
|
||||
|
@ -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:
|
||||
|
@ -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 '
|
||||
|
@ -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')
|
||||
|
@ -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'])
|
||||
|
@ -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
|
||||
|
32
releasenotes/notes/http-basic-auth-f8c0536eba989918.yaml
Normal file
32
releasenotes/notes/http-basic-auth-f8c0536eba989918.yaml
Normal 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
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user