Implement secure RBAC

The default policy will been replaced with one which aligns with the
Secure-RBAC scopes and roles. Since ironic-inspector is a tool used only
by system-level admins, only the ``system`` scope is supported, and the
only roles in the policy rules are ``admin`` and ``reader``.

The is_admin and is_observer rules are deprecated for removal, and
every rule which refers to them are deprecated in favor of the
system-scoped equivalent (system_scope:all with role:admin or
role:reader)

No unit tests covered the existing policy, these are now covered by
test_acl.TestACLDeprecated.

Change-Id: I4d038245c6b97b1504fb47eeec78ad3f9e5a897c
This commit is contained in:
Steve Baker 2021-01-28 09:41:39 +13:00
parent 5c79d7552a
commit c9e312f8b4
3 changed files with 589 additions and 43 deletions

View File

@ -16,6 +16,7 @@ import sys
from oslo_concurrency import lockutils
from oslo_config import cfg
from oslo_log import versionutils
from oslo_policy import opts
from oslo_policy import policy
@ -23,23 +24,52 @@ CONF = cfg.CONF
_ENFORCER = None
# TODO(gmann): Remove setting the default value of config policy_file
# once oslo_policy change the default value to 'policy.yaml'.
# https://github.com/openstack/oslo.policy/blob/a626ad12fe5a3abd49d70e3e5b95589d279ab578/oslo_policy/opts.py#L49
DEFAULT_POLICY_FILE = 'policy.yaml'
opts.set_defaults(cfg.CONF, DEFAULT_POLICY_FILE)
# Generic policy check string for system administrators. These are the people
# who need the highest level of authorization to operate the deployment.
# They're allowed to create, read, update, or delete any system-specific
# resource. They can also operate on project-specific resources where
# applicable (e.g., cleaning up baremetal hosts)
SYSTEM_ADMIN = 'role:admin and system_scope:all'
# Generic policy check string for system users who don't require all the
# authorization that system administrators typically have. This persona, or
# check string, typically isn't used by default, but it's existence it useful
# in the event a deployment wants to offload some administrative action from
# system administrator to system members
SYSTEM_MEMBER = 'role:member and system_scope:all'
# Generic policy check string for read-only access to system-level resources.
# This persona is useful for someone who needs access for auditing or even
# support. These uses are also able to view project-specific resources where
# applicable (e.g., listing all volumes in the deployment, regardless of the
# project they belong to).
SYSTEM_READER = 'role:reader and system_scope:all'
deprecated_node_reason = """
The inspector API is now aware of system scope and default roles.
"""
default_policies = [
policy.RuleDefault(
'is_admin',
'role:admin or role:administrator or role:baremetal_admin',
description='Full read/write API access'),
description='Full read/write API access',
deprecated_for_removal=True,
deprecated_reason=deprecated_node_reason,
deprecated_since=versionutils.deprecated.WALLABY),
policy.RuleDefault(
'is_observer',
'role:baremetal_observer',
description='Read-only API access'),
description='Read-only API access',
deprecated_for_removal=True,
deprecated_reason=deprecated_node_reason,
deprecated_since=versionutils.deprecated.WALLABY),
policy.RuleDefault(
'public_api',
'is_public_api:True',
@ -66,67 +96,126 @@ api_version_policies = [
]
deprecated_introspection_status = policy.DeprecatedRule(
name='introspection:status',
check_str='rule:is_admin or rule:is_observer'
)
deprecated_introspection_start = policy.DeprecatedRule(
name='introspection:start',
check_str='rule:is_admin'
)
deprecated_introspection_abort = policy.DeprecatedRule(
name='introspection:abort',
check_str='rule:is_admin'
)
deprecated_introspection_data = policy.DeprecatedRule(
name='introspection:data',
check_str='rule:is_admin'
)
deprecated_introspection_reapply = policy.DeprecatedRule(
name='introspection:reapply',
check_str='rule:is_admin'
)
deprecated_introspection_rule_get = policy.DeprecatedRule(
name='introspection:rule:get',
check_str='rule:is_admin'
)
deprecated_introspection_rule_delete = policy.DeprecatedRule(
name='introspection:rule:delete',
check_str='rule:is_admin'
)
deprecated_introspection_rule_create = policy.DeprecatedRule(
name='introspection:rule:create',
check_str='rule:is_admin'
)
introspection_policies = [
policy.DocumentedRuleDefault(
'introspection:continue',
'rule:public_api',
'Ramdisk callback to continue introspection',
[{'path': '/continue', 'method': 'POST'}]
name='introspection:continue',
check_str='rule:public_api',
description='Ramdisk callback to continue introspection',
operations=[{'path': '/continue', 'method': 'POST'}],
),
policy.DocumentedRuleDefault(
'introspection:status',
'rule:is_admin or rule:is_observer',
'Get introspection status',
[{'path': '/introspection', 'method': 'GET'},
{'path': '/introspection/{node_id}', 'method': 'GET'}]
name='introspection:status',
check_str=SYSTEM_READER,
description='Get introspection status',
operations=[{'path': '/introspection', 'method': 'GET'},
{'path': '/introspection/{node_id}', 'method': 'GET'}],
deprecated_rule=deprecated_introspection_status,
deprecated_reason=deprecated_node_reason,
deprecated_since=versionutils.deprecated.WALLABY
),
policy.DocumentedRuleDefault(
'introspection:start',
'rule:is_admin',
'Start introspection',
[{'path': '/introspection/{node_id}', 'method': 'POST'}]
name='introspection:start',
check_str=SYSTEM_ADMIN,
description='Start introspection',
operations=[{'path': '/introspection/{node_id}', 'method': 'POST'}],
deprecated_rule=deprecated_introspection_start,
deprecated_reason=deprecated_node_reason,
deprecated_since=versionutils.deprecated.WALLABY
),
policy.DocumentedRuleDefault(
'introspection:abort',
'rule:is_admin',
'Abort introspection',
[{'path': '/introspection/{node_id}/abort', 'method': 'POST'}]
name='introspection:abort',
check_str=SYSTEM_ADMIN,
description='Abort introspection',
operations=[{'path': '/introspection/{node_id}/abort',
'method': 'POST'}],
deprecated_rule=deprecated_introspection_abort,
deprecated_reason=deprecated_node_reason,
deprecated_since=versionutils.deprecated.WALLABY
),
policy.DocumentedRuleDefault(
'introspection:data',
'rule:is_admin',
'Get introspection data',
[{'path': '/introspection/{node_id}/data', 'method': 'GET'}]
name='introspection:data',
check_str=SYSTEM_ADMIN,
description='Get introspection data',
operations=[{'path': '/introspection/{node_id}/data',
'method': 'GET'}],
deprecated_rule=deprecated_introspection_data,
deprecated_reason=deprecated_node_reason,
deprecated_since=versionutils.deprecated.WALLABY
),
policy.DocumentedRuleDefault(
'introspection:reapply',
'rule:is_admin',
'Reapply introspection on stored data',
[{'path': '/introspection/{node_id}/data/unprocessed',
'method': 'POST'}]
name='introspection:reapply',
check_str=SYSTEM_ADMIN,
description='Reapply introspection on stored data',
operations=[{'path': '/introspection/{node_id}/data/unprocessed',
'method': 'POST'}],
deprecated_rule=deprecated_introspection_reapply,
deprecated_reason=deprecated_node_reason,
deprecated_since=versionutils.deprecated.WALLABY
),
]
rule_policies = [
policy.DocumentedRuleDefault(
'introspection:rule:get',
'rule:is_admin',
'Get introspection rule(s)',
[{'path': '/rules', 'method': 'GET'},
{'path': '/rules/{rule_id}', 'method': 'GET'}]
name='introspection:rule:get',
check_str=SYSTEM_ADMIN,
description='Get introspection rule(s)',
operations=[{'path': '/rules', 'method': 'GET'},
{'path': '/rules/{rule_id}', 'method': 'GET'}],
deprecated_rule=deprecated_introspection_rule_get,
deprecated_reason=deprecated_node_reason,
deprecated_since=versionutils.deprecated.WALLABY
),
policy.DocumentedRuleDefault(
'introspection:rule:delete',
'rule:is_admin',
'Delete introspection rule(s)',
[{'path': '/rules', 'method': 'DELETE'},
{'path': '/rules/{rule_id}', 'method': 'DELETE'}]
name='introspection:rule:delete',
check_str=SYSTEM_ADMIN,
description='Delete introspection rule(s)',
operations=[{'path': '/rules', 'method': 'DELETE'},
{'path': '/rules/{rule_id}', 'method': 'DELETE'}],
deprecated_rule=deprecated_introspection_rule_delete,
deprecated_reason=deprecated_node_reason,
deprecated_since=versionutils.deprecated.WALLABY
),
policy.DocumentedRuleDefault(
'introspection:rule:create',
'rule:is_admin',
'Create introspection rule',
[{'path': '/rules', 'method': 'POST'}]
name='introspection:rule:create',
check_str=SYSTEM_ADMIN,
description='Create introspection rule',
operations=[{'path': '/rules', 'method': 'POST'}],
deprecated_rule=deprecated_introspection_rule_create,
deprecated_reason=deprecated_node_reason,
deprecated_since=versionutils.deprecated.WALLABY
),
]

View File

@ -0,0 +1,438 @@
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT 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 datetime
from unittest import mock
import uuid
import fixtures
from keystoneauth1.fixture import v3 as v3_token
from keystonemiddleware import auth_token
from oslo_config import cfg
from oslo_context import context as oslo_context
import oslo_messaging as messaging
from oslo_utils import uuidutils
from ironic_inspector.common import rpc
import ironic_inspector.conf
from ironic_inspector import introspection_state as istate
from ironic_inspector import main
from ironic_inspector import node_cache
from ironic_inspector.test import base as test_base
CONF = ironic_inspector.conf.CONF
# Tokens for RBAC policy tests
ADMIN_TOKEN = uuid.uuid4().hex
admin_context = oslo_context.RequestContext(
user_id=ADMIN_TOKEN,
roles=['admin', 'member', 'reader'],
)
MEMBER_TOKEN = uuid.uuid4().hex
member_context = oslo_context.RequestContext(
user_id=MEMBER_TOKEN,
roles=['member', 'reader'],
)
READER_TOKEN = uuid.uuid4().hex
reader_context = oslo_context.RequestContext(
user_id=READER_TOKEN,
roles=['reader'],
)
NO_ROLE_TOKEN = uuid.uuid4().hex
no_role_context = oslo_context.RequestContext(
user_id=READER_TOKEN,
roles=[],
)
# Tokens for deprecated policy tests
BM_ADMIN_TOKEN = uuid.uuid4().hex
bm_admin_context = oslo_context.RequestContext(
user_id=BM_ADMIN_TOKEN,
roles=['baremetal_admin'],
)
BM_OBSERVER_TOKEN = uuid.uuid4().hex
bm_observer_context = oslo_context.RequestContext(
user_id=BM_OBSERVER_TOKEN,
roles=['baremetal_observer'],
)
USERS = {
ADMIN_TOKEN: admin_context.to_dict(),
MEMBER_TOKEN: member_context.to_dict(),
READER_TOKEN: reader_context.to_dict(),
NO_ROLE_TOKEN: no_role_context.to_dict(),
BM_ADMIN_TOKEN: bm_admin_context.to_dict(),
BM_OBSERVER_TOKEN: bm_observer_context.to_dict(),
}
class BasePolicyTest(test_base.BaseTest):
def init_app(self):
CONF.set_override('auth_strategy', 'keystone')
main._app.testing = True
self.app = main.get_app().test_client()
def setUp(self):
super(BasePolicyTest, self).setUp()
self.init_app()
self.uuid = uuidutils.generate_uuid()
self.rpc_get_client_mock = self.useFixture(
fixtures.MockPatchObject(rpc, 'get_client', autospec=True)).mock
self.client_mock = mock.MagicMock(spec=messaging.RPCClient)
self.rpc_get_client_mock.return_value = self.client_mock
self.fake_token = None
mock_auth = mock.patch.object(
auth_token.AuthProtocol, 'process_request',
autospec=True)
self.mock_auth = mock_auth.start()
self.addCleanup(mock_auth.stop)
self.mock_auth.side_effect = self._fake_process_request
mock_get = mock.patch.object(node_cache, 'get_node', autospec=True)
get = mock_get.start()
self.addCleanup(mock_get.stop)
get.return_value = node_cache.NodeInfo(
uuid=self.uuid,
started_at=datetime.datetime(1, 1, 1),
state=istate.States.processing)
def _fake_process_request(self, request, meow):
if self.fake_token:
request.user_token_valid = True
request.user_token = True
# is this right?!?
request.token_info = self.fake_token
request.auth_token = v3_token.Token(
user_id=self.fake_token['user'])
else:
# Because of this, the user will always get a 403 in testing, even
# if the API would normally return a 401 if a token is valid
request.user_token_valid = False
def set_token(self, token):
self.fake_token = USERS[token]
headers = {
'X-Auth-Token': token,
'X-Roles': ','.join(self.fake_token['roles'])
}
if cfg.CONF.oslo_policy.enforce_scope:
headers['OpenStack-System-Scope'] = 'all'
return headers
def assert_status(self, status_code, token, request_func, path, data=None):
headers = self.set_token(token)
res = request_func(path, headers=headers, data=data)
self.assertEqual(status_code, res.status_code)
class TestACLDeprecated(BasePolicyTest):
def setUp(self):
super(TestACLDeprecated, self).setUp()
cfg.CONF.set_override('enforce_scope', False, group='oslo_policy')
cfg.CONF.set_override('enforce_new_defaults', False,
group='oslo_policy')
def test_root_baremetal_admin(self):
self.assert_status(200, BM_ADMIN_TOKEN, self.app.get, '/')
self.assert_status(200, BM_ADMIN_TOKEN, self.app.get, '/v1')
def test_root_baremetal_observer(self):
self.assert_status(200, BM_OBSERVER_TOKEN, self.app.get, '/')
self.assert_status(200, BM_OBSERVER_TOKEN, self.app.get, '/v1')
def test_root_system_no_role(self):
self.assert_status(200, NO_ROLE_TOKEN, self.app.get, '/')
self.assert_status(200, NO_ROLE_TOKEN, self.app.get, '/v1')
def test_introspect_baremetal_admin(self):
self.assert_status(202, BM_ADMIN_TOKEN, self.app.post,
'/v1/introspection/%s' % self.uuid)
def test_introspect_baremetal_observer(self):
self.assert_status(403, BM_OBSERVER_TOKEN, self.app.post,
'/v1/introspection/%s/abort' % self.uuid)
def test_abort_baremetal_admin(self):
self.assert_status(202, BM_ADMIN_TOKEN, self.app.post,
'/v1/introspection/%s/abort' % self.uuid)
def test_abort_baremetal_observer(self):
self.assert_status(403, BM_OBSERVER_TOKEN, self.app.post,
'/v1/introspection/%s/abort' % self.uuid)
def test_status_baremetal_admin(self):
self.assert_status(200, BM_ADMIN_TOKEN, self.app.get,
'/v1/introspection/%s' % self.uuid)
def test_status_baremetal_observer(self):
self.assert_status(200, BM_OBSERVER_TOKEN, self.app.get,
'/v1/introspection/%s' % self.uuid)
def test_list_baremetal_admin(self):
self.assert_status(200, BM_ADMIN_TOKEN, self.app.get,
'/v1/introspection')
def test_list_baremetal_observer(self):
self.assert_status(200, BM_OBSERVER_TOKEN, self.app.get,
'/v1/introspection')
def test_data_baremetal_admin(self):
self.assert_status(404, BM_ADMIN_TOKEN, self.app.get,
'/v1/introspection/%s/data' % self.uuid)
def test_data_baremetal_observer(self):
self.assert_status(403, BM_OBSERVER_TOKEN, self.app.get,
'/v1/introspection/%s/data' % self.uuid)
def test_data_unprocessed_baremetal_admin(self):
self.assert_status(400, BM_ADMIN_TOKEN, self.app.post,
'/v1/introspection/%s/data/unprocessed' % self.uuid,
data={'foo': 'bar'})
def test_data_unprocessed_baremetal_observer(self):
self.assert_status(403, BM_OBSERVER_TOKEN, self.app.post,
'/v1/introspection/%s/data/unprocessed' % self.uuid,
data={'foo': 'bar'})
def test_rule_list_baremetal_admin(self):
self.assert_status(200, BM_ADMIN_TOKEN, self.app.get,
'/v1/rules')
def test_rule_list_baremetal_observer(self):
self.assert_status(403, BM_OBSERVER_TOKEN, self.app.get,
'/v1/rules')
def test_rule_get_baremetal_admin(self):
self.assert_status(404, BM_ADMIN_TOKEN, self.app.get,
'/v1/rules/foo')
def test_rule_get_baremetal_observer(self):
self.assert_status(403, BM_OBSERVER_TOKEN, self.app.get,
'/v1/rules/foo')
def test_rule_delete_all_baremetal_admin(self):
self.assert_status(204, BM_ADMIN_TOKEN, self.app.delete,
'/v1/rules')
def test_rule_delete_all_baremetal_observer(self):
self.assert_status(403, BM_OBSERVER_TOKEN, self.app.delete,
'/v1/rules')
def test_rule_delete_baremetal_admin(self):
self.assert_status(404, BM_ADMIN_TOKEN, self.app.delete,
'/v1/rules/foo')
def test_rule_delete_baremetal_observer(self):
self.assert_status(403, BM_OBSERVER_TOKEN, self.app.delete,
'/v1/rules/foo')
def test_rule_create_baremetal_admin(self):
self.assert_status(500, BM_ADMIN_TOKEN, self.app.post,
'/v1/rules',
data={
'uuid': self.uuid,
'conditions': 'cond',
'actions': 'act'
})
def test_rule_create_baremetal_observer(self):
self.assert_status(403, BM_OBSERVER_TOKEN, self.app.post,
'/v1/rules',
data={
'uuid': self.uuid,
'conditions': 'cond',
'actions': 'act'
})
class TestRBACScoped(BasePolicyTest):
def setUp(self):
super(TestRBACScoped, self).setUp()
cfg.CONF.set_override('enforce_scope', True, group='oslo_policy')
cfg.CONF.set_override('enforce_new_defaults', True,
group='oslo_policy')
def test_root_system_admin(self):
self.assert_status(200, ADMIN_TOKEN, self.app.get, '/')
self.assert_status(200, ADMIN_TOKEN, self.app.get, '/v1')
def test_root_system_member(self):
self.assert_status(200, MEMBER_TOKEN, self.app.get, '/')
self.assert_status(200, MEMBER_TOKEN, self.app.get, '/v1')
def test_root_system_reader(self):
self.assert_status(200, READER_TOKEN, self.app.get, '/')
self.assert_status(200, READER_TOKEN, self.app.get, '/v1')
def test_root_system_no_role(self):
self.assert_status(200, NO_ROLE_TOKEN, self.app.get, '/')
self.assert_status(200, NO_ROLE_TOKEN, self.app.get, '/v1')
def test_introspect_system_admin(self):
self.assert_status(202, ADMIN_TOKEN, self.app.post,
'/v1/introspection/%s' % self.uuid)
def test_introspect_system_member(self):
self.assert_status(403, MEMBER_TOKEN, self.app.post,
'/v1/introspection/%s' % self.uuid)
def test_introspect_system_reader(self):
self.assert_status(403, READER_TOKEN, self.app.post,
'/v1/introspection/%s/abort' % self.uuid)
def test_abort_system_admin(self):
self.assert_status(202, ADMIN_TOKEN, self.app.post,
'/v1/introspection/%s/abort' % self.uuid)
def test_abort_system_member(self):
self.assert_status(403, MEMBER_TOKEN, self.app.post,
'/v1/introspection/%s/abort' % self.uuid)
def test_abort_system_reader(self):
self.assert_status(403, READER_TOKEN, self.app.post,
'/v1/introspection/%s/abort' % self.uuid)
def test_status_system_admin(self):
self.assert_status(200, ADMIN_TOKEN, self.app.get,
'/v1/introspection/%s' % self.uuid)
def test_status_system_member(self):
self.assert_status(200, MEMBER_TOKEN, self.app.get,
'/v1/introspection/%s' % self.uuid)
def test_status_system_reader(self):
self.assert_status(200, READER_TOKEN, self.app.get,
'/v1/introspection/%s' % self.uuid)
def test_list_system_admin(self):
self.assert_status(200, ADMIN_TOKEN, self.app.get,
'/v1/introspection')
def test_list_system_member(self):
self.assert_status(200, MEMBER_TOKEN, self.app.get,
'/v1/introspection')
def test_list_system_reader(self):
self.assert_status(200, READER_TOKEN, self.app.get,
'/v1/introspection')
def test_data_system_admin(self):
self.assert_status(404, ADMIN_TOKEN, self.app.get,
'/v1/introspection/%s/data' % self.uuid)
def test_data_system_member(self):
self.assert_status(403, MEMBER_TOKEN, self.app.get,
'/v1/introspection/%s/data' % self.uuid)
def test_data_system_reader(self):
self.assert_status(403, READER_TOKEN, self.app.get,
'/v1/introspection/%s/data' % self.uuid)
def test_data_unprocessed_system_admin(self):
self.assert_status(400, ADMIN_TOKEN, self.app.post,
'/v1/introspection/%s/data/unprocessed' % self.uuid,
data={'foo': 'bar'})
def test_data_unprocessed_system_member(self):
self.assert_status(403, MEMBER_TOKEN, self.app.post,
'/v1/introspection/%s/data/unprocessed' % self.uuid,
data={'foo': 'bar'})
def test_data_unprocessed_system_reader(self):
self.assert_status(403, READER_TOKEN, self.app.post,
'/v1/introspection/%s/data/unprocessed' % self.uuid,
data={'foo': 'bar'})
def test_rule_list_system_admin(self):
self.assert_status(200, ADMIN_TOKEN, self.app.get,
'/v1/rules')
def test_rule_list_system_member(self):
self.assert_status(403, MEMBER_TOKEN, self.app.get,
'/v1/rules')
def test_rule_list_system_reader(self):
self.assert_status(403, READER_TOKEN, self.app.get,
'/v1/rules')
def test_rule_get_system_admin(self):
self.assert_status(404, ADMIN_TOKEN, self.app.get,
'/v1/rules/foo')
def test_rule_get_system_member(self):
self.assert_status(403, MEMBER_TOKEN, self.app.get,
'/v1/rules/foo')
def test_rule_get_system_reader(self):
self.assert_status(403, READER_TOKEN, self.app.get,
'/v1/rules/foo')
def test_rule_delete_all_system_admin(self):
self.assert_status(204, ADMIN_TOKEN, self.app.delete,
'/v1/rules')
def test_rule_delete_all_system_member(self):
self.assert_status(403, MEMBER_TOKEN, self.app.delete,
'/v1/rules')
def test_rule_delete_all_system_reader(self):
self.assert_status(403, READER_TOKEN, self.app.delete,
'/v1/rules')
def test_rule_delete_system_admin(self):
self.assert_status(404, ADMIN_TOKEN, self.app.delete,
'/v1/rules/foo')
def test_rule_delete_system_member(self):
self.assert_status(403, MEMBER_TOKEN, self.app.delete,
'/v1/rules/foo')
def test_rule_delete_system_reader(self):
self.assert_status(403, READER_TOKEN, self.app.delete,
'/v1/rules/foo')
def test_rule_create_system_admin(self):
self.assert_status(500, ADMIN_TOKEN, self.app.post,
'/v1/rules',
data={
'uuid': self.uuid,
'conditions': 'cond',
'actions': 'act'
})
def test_rule_create_system_member(self):
self.assert_status(403, MEMBER_TOKEN, self.app.post,
'/v1/rules',
data={
'uuid': self.uuid,
'conditions': 'cond',
'actions': 'act'
})
def test_rule_create_system_reader(self):
self.assert_status(403, READER_TOKEN, self.app.post,
'/v1/rules',
data={
'uuid': self.uuid,
'conditions': 'cond',
'actions': 'act'
})

View File

@ -0,0 +1,19 @@
---
features:
- |
The default policy will been replaced with one which aligns with the
Secure-RBAC scopes and roles. Since ironic-inspector is a tool used only
by system-level admins, only the ``system`` scope is supported, and the
only roles in the policy rules are ``admin`` and ``reader``.
upgrade:
- |
The new policy is only enforced when ``[oslo_policy]`` config is changed to
``enforce_new_defaults=True`` and ``enforce_scope=True``, otherwise the
existing deprecated policy is used. User accounts which rely on having
the ``baremetal_admin`` or ``baremetal_observer`` roles will need to
have system-scoped ``admin`` or ``reader`` roles to use the API when the
new policy is enforced.
deprecations:
- |
The previous policy is still enforced by default, but is now deprecated
and will be removed in a future release.