Enable HTTP Basic authentication for JSON-RPC
Change-Id: I90c4d5ef925c1dbb120948e3c0fe5982c9d997a0 Story: 2007656 Task: 39827
This commit is contained in:
parent
62408b32ae
commit
350d84ed41
doc/source/install
ironic
@ -12,7 +12,7 @@ You should make the following changes to ``/etc/ironic/ironic.conf``:
|
||||
...
|
||||
auth_strategy=noauth
|
||||
|
||||
Another options is ``http_basic`` where the credentials are stored in an
|
||||
Another option is ``http_basic`` where the credentials are stored in an
|
||||
`Apache htpasswd format`_ file::
|
||||
|
||||
[DEFAULT]
|
||||
@ -52,6 +52,27 @@ You should make the following changes to ``/etc/ironic/ironic.conf``:
|
||||
[DEFAULT]
|
||||
rpc_transport = json-rpc
|
||||
|
||||
JSON RPC also has its own authentication strategy. If it is not specified then
|
||||
the stategy defaults to ``[DEFAULT]`` ``auth_strategy``. The following will
|
||||
set JSON RPC to ``noauth``::
|
||||
|
||||
[json_rpc]
|
||||
auth_strategy=noauth
|
||||
|
||||
For ``http_basic`` the conductor server needs a credentials file to validate
|
||||
requests::
|
||||
|
||||
[json_rpc]
|
||||
auth_strategy=http_basic
|
||||
http_basic_auth_user_file=/etc/ironic/htpasswd-json-rpc
|
||||
|
||||
The API server also needs client-side credentials to be specified::
|
||||
|
||||
[json_rpc]
|
||||
auth_strategy=http_basic
|
||||
http_basic_username=myName
|
||||
http_basic_password=myPassword
|
||||
|
||||
If you don't use Image service, it's possible to provide images to Bare Metal
|
||||
service via a URL.
|
||||
|
||||
|
@ -16,5 +16,5 @@ from oslo_config import cfg
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
def require_authentication():
|
||||
return (CONF.json_rpc.auth_strategy or CONF.auth_strategy) == 'keystone'
|
||||
def auth_strategy():
|
||||
return CONF.json_rpc.auth_strategy or CONF.auth_strategy
|
||||
|
@ -15,6 +15,8 @@
|
||||
This client is compatible with any JSON RPC 2.0 implementation, including ours.
|
||||
"""
|
||||
|
||||
import base64
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log
|
||||
from oslo_utils import importutils
|
||||
@ -36,18 +38,27 @@ def _get_session():
|
||||
global _SESSION
|
||||
|
||||
if _SESSION is None:
|
||||
if json_rpc.require_authentication():
|
||||
auth_strategy = json_rpc.auth_strategy()
|
||||
if auth_strategy == 'keystone':
|
||||
auth = keystone.get_auth('json_rpc')
|
||||
else:
|
||||
auth = None
|
||||
|
||||
session = keystone.get_session('json_rpc', auth=auth)
|
||||
session.headers = {
|
||||
headers = {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
if auth_strategy == 'http_basic':
|
||||
token = '{}:{}'.format(
|
||||
CONF.json_rpc.http_basic_username,
|
||||
CONF.json_rpc.http_basic_password
|
||||
).encode('utf-8')
|
||||
encoded = base64.b64encode(token).decode('utf-8')
|
||||
headers['Authorization'] = 'Basic {}'.format(encoded)
|
||||
|
||||
# Adds options like connect_retries
|
||||
_SESSION = keystone.get_adapter('json_rpc', session=session)
|
||||
_SESSION = keystone.get_adapter('json_rpc', session=session,
|
||||
additional_headers=headers)
|
||||
|
||||
return _SESSION
|
||||
|
||||
|
@ -21,6 +21,7 @@ https://www.jsonrpc.org/specification. Main differences:
|
||||
|
||||
import json
|
||||
|
||||
from ironic_lib import auth_basic
|
||||
from keystonemiddleware import auth_token
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log
|
||||
@ -90,9 +91,14 @@ class WSGIService(service.Service):
|
||||
self.manager = manager
|
||||
self.serializer = serializer
|
||||
self._method_map = _build_method_map(manager)
|
||||
if json_rpc.require_authentication():
|
||||
auth_strategy = json_rpc.auth_strategy()
|
||||
if auth_strategy == 'keystone':
|
||||
conf = dict(CONF.keystone_authtoken)
|
||||
app = auth_token.AuthProtocol(self._application, conf)
|
||||
elif auth_strategy == 'http_basic':
|
||||
app = auth_basic.BasicAuthMiddleware(
|
||||
self._application,
|
||||
cfg.CONF.json_rpc.http_basic_auth_user_file)
|
||||
else:
|
||||
app = self._application
|
||||
self.server = wsgi.Server(CONF, 'ironic-json-rpc', app,
|
||||
@ -109,7 +115,7 @@ class WSGIService(service.Service):
|
||||
return webob.Response(status_code=405, json_body=body)(
|
||||
environment, start_response)
|
||||
|
||||
if json_rpc.require_authentication():
|
||||
if json_rpc.auth_strategy() == 'keystone':
|
||||
roles = (request.headers.get('X-Roles') or '').split(',')
|
||||
if 'admin' not in roles:
|
||||
LOG.debug('Roles %s do not contain "admin", rejecting '
|
||||
|
@ -19,9 +19,14 @@ opts = [
|
||||
cfg.StrOpt('auth_strategy',
|
||||
choices=[('noauth', _('no authentication')),
|
||||
('keystone', _('use the Identity service for '
|
||||
'authentication'))],
|
||||
'authentication')),
|
||||
('http_basic', _('HTTP basic authentication'))],
|
||||
help=_('Authentication strategy used by JSON RPC. Defaults to '
|
||||
'the global auth_strategy setting.')),
|
||||
cfg.StrOpt('http_basic_auth_user_file',
|
||||
default='/etc/ironic/htpasswd-json-rpc',
|
||||
help=_('Path to Apache format user authentication file used '
|
||||
'when auth_strategy=http_basic')),
|
||||
cfg.HostAddressOpt('host_ip',
|
||||
default='::',
|
||||
help=_('The IP address or hostname on which JSON RPC '
|
||||
@ -32,6 +37,17 @@ opts = [
|
||||
cfg.BoolOpt('use_ssl',
|
||||
default=False,
|
||||
help=_('Whether to use TLS for JSON RPC')),
|
||||
cfg.StrOpt('http_basic_username',
|
||||
default='',
|
||||
help=_("Name of the user to use for HTTP Basic authentication "
|
||||
"client requests. Required when "
|
||||
"auth_strategy=http_basic.")),
|
||||
cfg.StrOpt('http_basic_password',
|
||||
default='',
|
||||
secret=True,
|
||||
help=_("Password to use for HTTP Basic authentication "
|
||||
"client requests. Required when "
|
||||
"auth_strategy=http_basic.")),
|
||||
]
|
||||
|
||||
|
||||
|
@ -10,6 +10,8 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
from unittest import mock
|
||||
|
||||
import fixtures
|
||||
@ -109,7 +111,7 @@ class TestService(test_base.TestCase):
|
||||
else:
|
||||
return response.json_body
|
||||
else:
|
||||
self.assertFalse(response.text)
|
||||
return response.text
|
||||
|
||||
def _check(self, body, result=None, error=None, request_id='abcd'):
|
||||
self.assertEqual('2.0', body.pop('jsonrpc'))
|
||||
@ -119,6 +121,33 @@ class TestService(test_base.TestCase):
|
||||
else:
|
||||
self.assertEqual({'result': result}, body)
|
||||
|
||||
def _setup_http_basic(self):
|
||||
with tempfile.NamedTemporaryFile(mode='w', delete=False) as f:
|
||||
f.write('myName:$2y$05$lE3eGtyj41jZwrzS87KTqe6.'
|
||||
'JETVCWBkc32C63UP2aYrGoYOEpbJm\n\n\n')
|
||||
self.addCleanup(os.remove, f.name)
|
||||
self.config(http_basic_auth_user_file=f.name, group='json_rpc')
|
||||
self.config(auth_strategy='http_basic', group='json_rpc')
|
||||
# self.config(http_basic_username='myUser', group='json_rpc')
|
||||
# self.config(http_basic_password='myPassword', group='json_rpc')
|
||||
self.service = server.WSGIService(FakeManager(), self.serializer)
|
||||
self.app = self.server_mock.call_args[0][2]
|
||||
|
||||
def test_http_basic_not_authenticated(self):
|
||||
self._setup_http_basic()
|
||||
self._request('success', {'context': self.ctx, 'x': 42},
|
||||
request_id=None, expected_error=401)
|
||||
|
||||
def test_http_basic(self):
|
||||
self._setup_http_basic()
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Basic bXlOYW1lOm15UGFzc3dvcmQ='
|
||||
}
|
||||
body = self._request('success', {'context': self.ctx, 'x': 42},
|
||||
headers=headers)
|
||||
self._check(body, result=42)
|
||||
|
||||
def test_success(self):
|
||||
body = self._request('success', {'context': self.ctx, 'x': 42})
|
||||
self._check(body, result=42)
|
||||
@ -130,7 +159,7 @@ class TestService(test_base.TestCase):
|
||||
def test_notification(self):
|
||||
body = self._request('no_result', {'context': self.ctx},
|
||||
request_id=None)
|
||||
self.assertIsNone(body)
|
||||
self.assertEqual('', body)
|
||||
|
||||
def test_no_context(self):
|
||||
body = self._request('no_context')
|
||||
@ -542,3 +571,70 @@ class TestClient(test_base.TestCase):
|
||||
'redfish_password': '***'})
|
||||
resp_text = mock_log.call_args_list[1][0][2]
|
||||
self.assertEqual(body.replace('passw0rd', '***'), resp_text)
|
||||
|
||||
|
||||
@mock.patch('ironic.common.json_rpc.client.keystone', autospec=True)
|
||||
class TestSession(test_base.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestSession, self).setUp()
|
||||
client._SESSION = None
|
||||
|
||||
def test_noauth(self, mock_keystone):
|
||||
self.config(auth_strategy='noauth', group='json_rpc')
|
||||
session = client._get_session()
|
||||
|
||||
mock_keystone.get_auth.assert_not_called()
|
||||
mock_keystone.get_session.assert_called_once_with(
|
||||
'json_rpc', auth=None)
|
||||
|
||||
internal_session = mock_keystone.get_session.return_value
|
||||
|
||||
mock_keystone.get_adapter.assert_called_once_with(
|
||||
'json_rpc',
|
||||
session=internal_session,
|
||||
additional_headers={
|
||||
'Content-Type': 'application/json'
|
||||
})
|
||||
self.assertEqual(mock_keystone.get_adapter.return_value, session)
|
||||
|
||||
def test_keystone(self, mock_keystone):
|
||||
self.config(auth_strategy='keystone', group='json_rpc')
|
||||
session = client._get_session()
|
||||
|
||||
mock_keystone.get_auth.assert_called_once_with('json_rpc')
|
||||
auth = mock_keystone.get_auth.return_value
|
||||
|
||||
mock_keystone.get_session.assert_called_once_with(
|
||||
'json_rpc', auth=auth)
|
||||
|
||||
internal_session = mock_keystone.get_session.return_value
|
||||
|
||||
mock_keystone.get_adapter.assert_called_once_with(
|
||||
'json_rpc',
|
||||
session=internal_session,
|
||||
additional_headers={
|
||||
'Content-Type': 'application/json'
|
||||
})
|
||||
self.assertEqual(mock_keystone.get_adapter.return_value, session)
|
||||
|
||||
def test_http_basic(self, mock_keystone):
|
||||
self.config(auth_strategy='http_basic', group='json_rpc')
|
||||
self.config(http_basic_username='myName', group='json_rpc')
|
||||
self.config(http_basic_password='myPassword', group='json_rpc')
|
||||
session = client._get_session()
|
||||
|
||||
mock_keystone.get_auth.assert_not_called()
|
||||
mock_keystone.get_session.assert_called_once_with(
|
||||
'json_rpc', auth=None)
|
||||
|
||||
internal_session = mock_keystone.get_session.return_value
|
||||
|
||||
mock_keystone.get_adapter.assert_called_once_with(
|
||||
'json_rpc',
|
||||
session=internal_session,
|
||||
additional_headers={
|
||||
'Authorization': 'Basic bXlOYW1lOm15UGFzc3dvcmQ=',
|
||||
'Content-Type': 'application/json'
|
||||
})
|
||||
self.assertEqual(mock_keystone.get_adapter.return_value, session)
|
||||
|
Loading…
x
Reference in New Issue
Block a user