Add support for multiple policy files
Most of the neutron plugins provide an updated version of policy.json file with full list of rules, but at the same time there are a lot of other plugins which provide their own policy files and store them in the policy.d/ folder: neutron-fwaas, networking-bgpvpn, vmware-nsx, ect... To implement the tests for such plugins the Patrole should be able to load and merge multiple policy files for any of the services. Modify the discover_policy_files function to discover all policy files for each of the services. Using glob.glob() function makes it possible to use patterns like '*.json' to discover the policy files. Modify the _get_policy_data function to load a data from all discovered policy files for a service. Update the unit test according to the changes. Change-Id: Ib24f3d6d7a5ffdeaecce579af9795fd897dce872
This commit is contained in:
parent
a3c15da1cd
commit
062fb157b8
|
@ -39,8 +39,9 @@ This option is paradoxical with the Tempest plugin architecture.
|
|||
help="""List of the paths to search for policy files. Each
|
||||
policy path assumes that the service name is included in the path once. Also
|
||||
assumes Patrole is on the same host as the policy files. The paths should be
|
||||
ordered by precedence, with high-priority paths before low-priority paths. The
|
||||
first path that is found to contain the service's policy file will be used.
|
||||
ordered by precedence, with high-priority paths before low-priority paths. All
|
||||
the paths that are found to contain the service's policy file will be used and
|
||||
all policy files will be merged.
|
||||
"""),
|
||||
cfg.BoolOpt('test_custom_requirements',
|
||||
default=False,
|
||||
|
|
|
@ -13,7 +13,9 @@
|
|||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import collections
|
||||
import copy
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
|
||||
|
@ -103,17 +105,14 @@ class PolicyAuthority(RbacAuthority):
|
|||
if extra_target_data is None:
|
||||
extra_target_data = {}
|
||||
|
||||
self.validate_service(service)
|
||||
self.service = self.validate_service(service)
|
||||
|
||||
# Prioritize dynamically searching for policy files over relying on
|
||||
# deprecated service-specific policy file locations.
|
||||
self.path = None
|
||||
if CONF.patrole.custom_policy_files:
|
||||
self.discover_policy_files()
|
||||
self.path = self.policy_files.get(service)
|
||||
|
||||
self.rules = policy.Rules.load(self._get_policy_data(service),
|
||||
'default')
|
||||
self.rules = policy.Rules.load(self._get_policy_data(), 'default')
|
||||
self.project_id = project_id
|
||||
self.user_id = user_id
|
||||
self.extra_target_data = extra_target_data
|
||||
|
@ -139,19 +138,22 @@ class PolicyAuthority(RbacAuthority):
|
|||
raise rbac_exceptions.RbacInvalidServiceException(
|
||||
"%s is NOT a valid service." % service)
|
||||
|
||||
return service
|
||||
|
||||
@classmethod
|
||||
def discover_policy_files(cls):
|
||||
"""Dynamically discover the policy file for each service in
|
||||
``cls.available_services``. Pick the first candidate path found
|
||||
``cls.available_services``. Pick all candidate paths found
|
||||
out of the potential paths in ``[patrole] custom_policy_files``.
|
||||
"""
|
||||
if not hasattr(cls, 'policy_files'):
|
||||
cls.policy_files = {}
|
||||
cls.policy_files = collections.defaultdict(list)
|
||||
for service in cls.available_services:
|
||||
for candidate_path in CONF.patrole.custom_policy_files:
|
||||
if os.path.isfile(candidate_path % service):
|
||||
cls.policy_files.setdefault(service,
|
||||
candidate_path % service)
|
||||
path = candidate_path % service
|
||||
for filename in glob.iglob(path):
|
||||
if os.path.isfile(filename):
|
||||
cls.policy_files[service].append(filename)
|
||||
|
||||
def allowed(self, rule_name, role):
|
||||
"""Checks if a given rule in a policy is allowed with given role.
|
||||
|
@ -168,17 +170,28 @@ class PolicyAuthority(RbacAuthority):
|
|||
is_admin=is_admin_context)
|
||||
return is_allowed
|
||||
|
||||
def _get_policy_data(self, service):
|
||||
def _get_policy_data(self):
|
||||
file_policy_data = {}
|
||||
mgr_policy_data = {}
|
||||
policy_data = {}
|
||||
|
||||
# Check whether policy file exists and attempt to read it.
|
||||
if self.path and os.path.isfile(self.path):
|
||||
for path in self.policy_files[self.service]:
|
||||
try:
|
||||
with open(self.path, 'r') as policy_file:
|
||||
file_policy_data = policy_file.read()
|
||||
file_policy_data = json.loads(file_policy_data)
|
||||
with open(path, 'r') as fp:
|
||||
for k, v in json.load(fp).items():
|
||||
if k not in file_policy_data:
|
||||
file_policy_data[k] = v
|
||||
else:
|
||||
# If the policy name and rule are the same, no
|
||||
# ambiguity, so no reason to warn.
|
||||
if v != file_policy_data[k]:
|
||||
LOG.warning(
|
||||
"The same policy name: %s was found in "
|
||||
"multiple policies files for service %s. "
|
||||
"This can lead to policy rule ambiguity. "
|
||||
"Using rule: %s", k, self.service,
|
||||
file_policy_data[k])
|
||||
except (IOError, ValueError) as e:
|
||||
msg = "Failed to read policy file for service. "
|
||||
if isinstance(e, IOError):
|
||||
|
@ -186,21 +199,20 @@ class PolicyAuthority(RbacAuthority):
|
|||
else:
|
||||
msg += "JSON may be improperly formatted."
|
||||
LOG.debug(msg)
|
||||
file_policy_data = {}
|
||||
|
||||
# Check whether policy actions are defined in code. Nova and Keystone,
|
||||
# for example, define their default policy actions in code.
|
||||
mgr = stevedore.named.NamedExtensionManager(
|
||||
'oslo.policy.policies',
|
||||
names=[service],
|
||||
names=[self.service],
|
||||
on_load_failure_callback=None,
|
||||
invoke_on_load=True,
|
||||
warn_on_missing_entrypoint=False)
|
||||
|
||||
if mgr:
|
||||
policy_generator = {policy.name: policy.obj for policy in mgr}
|
||||
if policy_generator and service in policy_generator:
|
||||
for rule in policy_generator[service]:
|
||||
policy_generator = {plc.name: plc.obj for plc in mgr}
|
||||
if policy_generator and self.service in policy_generator:
|
||||
for rule in policy_generator[self.service]:
|
||||
mgr_policy_data[rule.name] = str(rule.check)
|
||||
|
||||
# If data from both file and code exist, combine both together.
|
||||
|
@ -217,10 +229,10 @@ class PolicyAuthority(RbacAuthority):
|
|||
policy_data = mgr_policy_data
|
||||
else:
|
||||
error_message = (
|
||||
'Policy file for {0} service was not found among the '
|
||||
'Policy files for {0} service were not found among the '
|
||||
'registered in-code policies or in any of the possible policy '
|
||||
'files: {1}.'.format(service,
|
||||
[loc % service for loc in
|
||||
'files: {1}.'.format(self.service,
|
||||
[loc % self.service for loc in
|
||||
CONF.patrole.custom_policy_files])
|
||||
)
|
||||
raise rbac_exceptions.RbacParsingException(error_message)
|
||||
|
@ -228,8 +240,8 @@ class PolicyAuthority(RbacAuthority):
|
|||
try:
|
||||
policy_data = json.dumps(policy_data)
|
||||
except (TypeError, ValueError):
|
||||
error_message = 'Policy file for {0} service is invalid.'.format(
|
||||
service)
|
||||
error_message = 'Policy files for {0} service are invalid.'.format(
|
||||
self.service)
|
||||
raise rbac_exceptions.RbacParsingException(error_message)
|
||||
|
||||
return policy_data
|
||||
|
@ -296,9 +308,11 @@ class PolicyAuthority(RbacAuthority):
|
|||
|
||||
def _try_rule(self, apply_rule, target, access_data, o):
|
||||
if apply_rule not in self.rules:
|
||||
message = ("Policy action \"{0}\" not found in policy file: {1} or"
|
||||
" among registered policy in code defaults for service."
|
||||
).format(apply_rule, self.path)
|
||||
message = ('Policy action "{0}" not found in policy files: '
|
||||
'{1} or among registered policy in code defaults for '
|
||||
'{2} service.').format(apply_rule,
|
||||
self.policy_files[self.service],
|
||||
self.service)
|
||||
LOG.debug(message)
|
||||
raise rbac_exceptions.RbacParsingException(message)
|
||||
else:
|
||||
|
|
|
@ -270,9 +270,9 @@ class PolicyAuthorityTest(base.TestCase):
|
|||
|
||||
fake_rule = 'fake_rule'
|
||||
expected_message = (
|
||||
"Policy action \"{0}\" not found in policy file: {1} or among "
|
||||
"registered policy in code defaults for service.").format(
|
||||
fake_rule, self.custom_policy_file)
|
||||
'Policy action "{0}" not found in policy files: {1} or among '
|
||||
'registered policy in code defaults for {2} service.').format(
|
||||
fake_rule, [self.custom_policy_file], "custom_rbac_policy")
|
||||
|
||||
e = self.assertRaises(rbac_exceptions.RbacParsingException,
|
||||
authority.allowed, fake_rule, None)
|
||||
|
@ -292,9 +292,10 @@ class PolicyAuthorityTest(base.TestCase):
|
|||
mock.sentinel.error)})
|
||||
|
||||
expected_message = (
|
||||
"Policy action \"{0}\" not found in policy file: {1} or among "
|
||||
"registered policy in code defaults for service.").format(
|
||||
mock.sentinel.rule, self.custom_policy_file)
|
||||
'Policy action "{0}" not found in policy files: {1} or among '
|
||||
'registered policy in code defaults for {2} service.').format(
|
||||
mock.sentinel.rule, [self.custom_policy_file],
|
||||
"custom_rbac_policy")
|
||||
|
||||
e = self.assertRaises(rbac_exceptions.RbacParsingException,
|
||||
authority.allowed, mock.sentinel.rule, None)
|
||||
|
@ -313,7 +314,7 @@ class PolicyAuthorityTest(base.TestCase):
|
|||
]
|
||||
|
||||
mock_manager = mock.Mock(obj=fake_policy_rules, __name__='foo')
|
||||
mock_manager.configure_mock(name='fake_service')
|
||||
mock_manager.configure_mock(name='tenant_rbac_policy')
|
||||
mock_stevedore.named.NamedExtensionManager.return_value = [
|
||||
mock_manager
|
||||
]
|
||||
|
@ -323,7 +324,7 @@ class PolicyAuthorityTest(base.TestCase):
|
|||
authority = policy_authority.PolicyAuthority(
|
||||
test_tenant_id, test_user_id, "tenant_rbac_policy")
|
||||
|
||||
policy_data = authority._get_policy_data('fake_service')
|
||||
policy_data = authority._get_policy_data()
|
||||
self.assertIsInstance(policy_data, str)
|
||||
|
||||
actual_policy_data = json.loads(policy_data)
|
||||
|
@ -354,7 +355,7 @@ class PolicyAuthorityTest(base.TestCase):
|
|||
]
|
||||
|
||||
mock_manager = mock.Mock(obj=fake_policy_rules, __name__='foo')
|
||||
mock_manager.configure_mock(name='fake_service')
|
||||
mock_manager.configure_mock(name='tenant_rbac_policy')
|
||||
mock_stevedore.named.NamedExtensionManager.return_value = [
|
||||
mock_manager
|
||||
]
|
||||
|
@ -364,7 +365,7 @@ class PolicyAuthorityTest(base.TestCase):
|
|||
|
||||
authority = policy_authority.PolicyAuthority(
|
||||
test_tenant_id, test_user_id, 'tenant_rbac_policy')
|
||||
policy_data = authority._get_policy_data('fake_service')
|
||||
policy_data = authority._get_policy_data()
|
||||
self.assertIsInstance(policy_data, str)
|
||||
|
||||
actual_policy_data = json.loads(policy_data)
|
||||
|
@ -388,7 +389,7 @@ class PolicyAuthorityTest(base.TestCase):
|
|||
None, None, 'test_service')
|
||||
|
||||
expected_error = (
|
||||
'Policy file for {0} service was not found among the registered '
|
||||
'Policy files for {0} service were not found among the registered '
|
||||
'in-code policies or in any of the possible policy files: {1}.'
|
||||
.format('test_service',
|
||||
[CONF.patrole.custom_policy_files[0] % 'test_service']))
|
||||
|
@ -413,7 +414,7 @@ class PolicyAuthorityTest(base.TestCase):
|
|||
policy_authority.PolicyAuthority,
|
||||
None, None, 'test_service')
|
||||
|
||||
expected_error = "Policy file for {0} service is invalid."\
|
||||
expected_error = "Policy files for {0} service are invalid."\
|
||||
.format("test_service")
|
||||
self.assertIn(expected_error, str(e))
|
||||
|
||||
|
@ -435,7 +436,7 @@ class PolicyAuthorityTest(base.TestCase):
|
|||
None, None, 'tenant_rbac_policy')
|
||||
|
||||
expected_error = (
|
||||
'Policy file for {0} service was not found among the registered '
|
||||
'Policy files for {0} service were not found among the registered '
|
||||
'in-code policies or in any of the possible policy files: {1}.'
|
||||
.format('tenant_rbac_policy', [CONF.patrole.custom_policy_files[0]
|
||||
% 'tenant_rbac_policy']))
|
||||
|
@ -450,7 +451,7 @@ class PolicyAuthorityTest(base.TestCase):
|
|||
dir(policy_authority.PolicyAuthority))
|
||||
self.assertIn('policy_files', dir(policy_parser))
|
||||
self.assertIn('tenant_rbac_policy', policy_parser.policy_files)
|
||||
self.assertEqual(self.conf_policy_path % 'tenant_rbac_policy',
|
||||
self.assertEqual([self.conf_policy_path % 'tenant_rbac_policy'],
|
||||
policy_parser.policy_files['tenant_rbac_policy'])
|
||||
|
||||
@mock.patch.object(policy_authority, 'policy', autospec=True)
|
||||
|
@ -458,35 +459,40 @@ class PolicyAuthorityTest(base.TestCase):
|
|||
autospec=True)
|
||||
@mock.patch.object(policy_authority, 'clients', autospec=True)
|
||||
@mock.patch.object(policy_authority, 'os', autospec=True)
|
||||
def test_discover_policy_files_with_many_invalid_one_valid(self, m_os,
|
||||
m_creds, *args):
|
||||
@mock.patch.object(policy_authority, 'glob', autospec=True)
|
||||
def test_discover_policy_files_with_many_invalid_one_valid(self, m_glob,
|
||||
m_os, m_creds,
|
||||
*args):
|
||||
service = 'test_service'
|
||||
custom_policy_files = ['foo/%s', 'bar/%s', 'baz/%s']
|
||||
m_glob.iglob.side_effect = [iter([path % service])
|
||||
for path in custom_policy_files]
|
||||
# Only the 3rd path is valid.
|
||||
m_os.path.isfile.side_effect = [False, False, True, False]
|
||||
m_os.path.isfile.side_effect = [False, False, True]
|
||||
|
||||
# Ensure the outer for loop runs only once in `discover_policy_files`.
|
||||
m_creds.Manager().identity_services_v3_client.\
|
||||
list_services.return_value = {
|
||||
'services': [{'name': 'test_service'}]}
|
||||
'services': [{'name': service}]}
|
||||
|
||||
# The expected policy will be 'baz/test_service'.
|
||||
self.useFixture(fixtures.ConfPatcher(
|
||||
custom_policy_files=['foo/%s', 'bar/%s', 'baz/%s'],
|
||||
custom_policy_files=custom_policy_files,
|
||||
group='patrole'))
|
||||
|
||||
policy_parser = policy_authority.PolicyAuthority(
|
||||
None, None, 'test_service')
|
||||
None, None, service)
|
||||
|
||||
# Ensure that "policy_files" is set at class and instance levels.
|
||||
self.assertIn('policy_files',
|
||||
dir(policy_authority.PolicyAuthority))
|
||||
self.assertIn('policy_files', dir(policy_parser))
|
||||
self.assertIn('test_service', policy_parser.policy_files)
|
||||
self.assertEqual('baz/test_service',
|
||||
policy_parser.policy_files['test_service'])
|
||||
self.assertTrue(hasattr(policy_authority.PolicyAuthority,
|
||||
'policy_files'))
|
||||
self.assertTrue(hasattr(policy_parser, 'policy_files'))
|
||||
self.assertEqual(['baz/%s' % service],
|
||||
policy_parser.policy_files[service])
|
||||
|
||||
def test_discover_policy_files_with_no_valid_files(self):
|
||||
expected_error = (
|
||||
'Policy file for {0} service was not found among the registered '
|
||||
'Policy files for {0} service were not found among the registered '
|
||||
'in-code policies or in any of the possible policy files: {1}.'
|
||||
.format('test_service', [self.conf_policy_path % 'test_service']))
|
||||
|
||||
|
@ -495,11 +501,11 @@ class PolicyAuthorityTest(base.TestCase):
|
|||
None, None, 'test_service')
|
||||
self.assertIn(expected_error, str(e))
|
||||
|
||||
self.assertIn('policy_files',
|
||||
dir(policy_authority.PolicyAuthority))
|
||||
self.assertNotIn(
|
||||
'test_service',
|
||||
policy_authority.PolicyAuthority.policy_files.keys())
|
||||
self.assertTrue(hasattr(policy_authority.PolicyAuthority,
|
||||
'policy_files'))
|
||||
self.assertEqual(
|
||||
[],
|
||||
policy_authority.PolicyAuthority.policy_files['test_service'])
|
||||
|
||||
def _test_validate_service(self, v2_services, v3_services,
|
||||
expected_failure=False, expected_services=None):
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
---
|
||||
features:
|
||||
- |
|
||||
In order to implement the tests for plugins which do not maintain the
|
||||
``policy.json`` with full list of the policy rules and provide policy file
|
||||
with only their own policy rules, the Patrole should be able to load and
|
||||
merge multiple policy files for any of the services.
|
||||
|
||||
- Discovery all policy files for each of the services.
|
||||
The updated ``discover_policy_files`` function picks all candidate paths
|
||||
found out of the potential paths in the ``[patrole].custom_policy_files``
|
||||
config option. Using ``glob.glob()`` function makes it possible to use
|
||||
the patterns like '\*.json' to discover the policy files.
|
||||
|
||||
- Loading and merging a data from multiple policy files.
|
||||
Patrole loads a data from each of the discovered policy files for a
|
||||
service and merge the data from all files.
|
Loading…
Reference in New Issue