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
|
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
|
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
|
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
|
ordered by precedence, with high-priority paths before low-priority paths. All
|
||||||
first path that is found to contain the service's policy file will be used.
|
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',
|
cfg.BoolOpt('test_custom_requirements',
|
||||||
default=False,
|
default=False,
|
||||||
|
|
|
@ -13,7 +13,9 @@
|
||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
import collections
|
||||||
import copy
|
import copy
|
||||||
|
import glob
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
@ -103,17 +105,14 @@ class PolicyAuthority(RbacAuthority):
|
||||||
if extra_target_data is None:
|
if extra_target_data is None:
|
||||||
extra_target_data = {}
|
extra_target_data = {}
|
||||||
|
|
||||||
self.validate_service(service)
|
self.service = self.validate_service(service)
|
||||||
|
|
||||||
# Prioritize dynamically searching for policy files over relying on
|
# Prioritize dynamically searching for policy files over relying on
|
||||||
# deprecated service-specific policy file locations.
|
# deprecated service-specific policy file locations.
|
||||||
self.path = None
|
|
||||||
if CONF.patrole.custom_policy_files:
|
if CONF.patrole.custom_policy_files:
|
||||||
self.discover_policy_files()
|
self.discover_policy_files()
|
||||||
self.path = self.policy_files.get(service)
|
|
||||||
|
|
||||||
self.rules = policy.Rules.load(self._get_policy_data(service),
|
self.rules = policy.Rules.load(self._get_policy_data(), 'default')
|
||||||
'default')
|
|
||||||
self.project_id = project_id
|
self.project_id = project_id
|
||||||
self.user_id = user_id
|
self.user_id = user_id
|
||||||
self.extra_target_data = extra_target_data
|
self.extra_target_data = extra_target_data
|
||||||
|
@ -139,19 +138,22 @@ class PolicyAuthority(RbacAuthority):
|
||||||
raise rbac_exceptions.RbacInvalidServiceException(
|
raise rbac_exceptions.RbacInvalidServiceException(
|
||||||
"%s is NOT a valid service." % service)
|
"%s is NOT a valid service." % service)
|
||||||
|
|
||||||
|
return service
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def discover_policy_files(cls):
|
def discover_policy_files(cls):
|
||||||
"""Dynamically discover the policy file for each service in
|
"""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``.
|
out of the potential paths in ``[patrole] custom_policy_files``.
|
||||||
"""
|
"""
|
||||||
if not hasattr(cls, 'policy_files'):
|
if not hasattr(cls, 'policy_files'):
|
||||||
cls.policy_files = {}
|
cls.policy_files = collections.defaultdict(list)
|
||||||
for service in cls.available_services:
|
for service in cls.available_services:
|
||||||
for candidate_path in CONF.patrole.custom_policy_files:
|
for candidate_path in CONF.patrole.custom_policy_files:
|
||||||
if os.path.isfile(candidate_path % service):
|
path = candidate_path % service
|
||||||
cls.policy_files.setdefault(service,
|
for filename in glob.iglob(path):
|
||||||
candidate_path % service)
|
if os.path.isfile(filename):
|
||||||
|
cls.policy_files[service].append(filename)
|
||||||
|
|
||||||
def allowed(self, rule_name, role):
|
def allowed(self, rule_name, role):
|
||||||
"""Checks if a given rule in a policy is allowed with given 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)
|
is_admin=is_admin_context)
|
||||||
return is_allowed
|
return is_allowed
|
||||||
|
|
||||||
def _get_policy_data(self, service):
|
def _get_policy_data(self):
|
||||||
file_policy_data = {}
|
file_policy_data = {}
|
||||||
mgr_policy_data = {}
|
mgr_policy_data = {}
|
||||||
policy_data = {}
|
policy_data = {}
|
||||||
|
|
||||||
# Check whether policy file exists and attempt to read it.
|
# 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:
|
try:
|
||||||
with open(self.path, 'r') as policy_file:
|
with open(path, 'r') as fp:
|
||||||
file_policy_data = policy_file.read()
|
for k, v in json.load(fp).items():
|
||||||
file_policy_data = json.loads(file_policy_data)
|
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:
|
except (IOError, ValueError) as e:
|
||||||
msg = "Failed to read policy file for service. "
|
msg = "Failed to read policy file for service. "
|
||||||
if isinstance(e, IOError):
|
if isinstance(e, IOError):
|
||||||
|
@ -186,21 +199,20 @@ class PolicyAuthority(RbacAuthority):
|
||||||
else:
|
else:
|
||||||
msg += "JSON may be improperly formatted."
|
msg += "JSON may be improperly formatted."
|
||||||
LOG.debug(msg)
|
LOG.debug(msg)
|
||||||
file_policy_data = {}
|
|
||||||
|
|
||||||
# Check whether policy actions are defined in code. Nova and Keystone,
|
# Check whether policy actions are defined in code. Nova and Keystone,
|
||||||
# for example, define their default policy actions in code.
|
# for example, define their default policy actions in code.
|
||||||
mgr = stevedore.named.NamedExtensionManager(
|
mgr = stevedore.named.NamedExtensionManager(
|
||||||
'oslo.policy.policies',
|
'oslo.policy.policies',
|
||||||
names=[service],
|
names=[self.service],
|
||||||
on_load_failure_callback=None,
|
on_load_failure_callback=None,
|
||||||
invoke_on_load=True,
|
invoke_on_load=True,
|
||||||
warn_on_missing_entrypoint=False)
|
warn_on_missing_entrypoint=False)
|
||||||
|
|
||||||
if mgr:
|
if mgr:
|
||||||
policy_generator = {policy.name: policy.obj for policy in mgr}
|
policy_generator = {plc.name: plc.obj for plc in mgr}
|
||||||
if policy_generator and service in policy_generator:
|
if policy_generator and self.service in policy_generator:
|
||||||
for rule in policy_generator[service]:
|
for rule in policy_generator[self.service]:
|
||||||
mgr_policy_data[rule.name] = str(rule.check)
|
mgr_policy_data[rule.name] = str(rule.check)
|
||||||
|
|
||||||
# If data from both file and code exist, combine both together.
|
# If data from both file and code exist, combine both together.
|
||||||
|
@ -217,10 +229,10 @@ class PolicyAuthority(RbacAuthority):
|
||||||
policy_data = mgr_policy_data
|
policy_data = mgr_policy_data
|
||||||
else:
|
else:
|
||||||
error_message = (
|
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 '
|
'registered in-code policies or in any of the possible policy '
|
||||||
'files: {1}.'.format(service,
|
'files: {1}.'.format(self.service,
|
||||||
[loc % service for loc in
|
[loc % self.service for loc in
|
||||||
CONF.patrole.custom_policy_files])
|
CONF.patrole.custom_policy_files])
|
||||||
)
|
)
|
||||||
raise rbac_exceptions.RbacParsingException(error_message)
|
raise rbac_exceptions.RbacParsingException(error_message)
|
||||||
|
@ -228,8 +240,8 @@ class PolicyAuthority(RbacAuthority):
|
||||||
try:
|
try:
|
||||||
policy_data = json.dumps(policy_data)
|
policy_data = json.dumps(policy_data)
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
error_message = 'Policy file for {0} service is invalid.'.format(
|
error_message = 'Policy files for {0} service are invalid.'.format(
|
||||||
service)
|
self.service)
|
||||||
raise rbac_exceptions.RbacParsingException(error_message)
|
raise rbac_exceptions.RbacParsingException(error_message)
|
||||||
|
|
||||||
return policy_data
|
return policy_data
|
||||||
|
@ -296,9 +308,11 @@ class PolicyAuthority(RbacAuthority):
|
||||||
|
|
||||||
def _try_rule(self, apply_rule, target, access_data, o):
|
def _try_rule(self, apply_rule, target, access_data, o):
|
||||||
if apply_rule not in self.rules:
|
if apply_rule not in self.rules:
|
||||||
message = ("Policy action \"{0}\" not found in policy file: {1} or"
|
message = ('Policy action "{0}" not found in policy files: '
|
||||||
" among registered policy in code defaults for service."
|
'{1} or among registered policy in code defaults for '
|
||||||
).format(apply_rule, self.path)
|
'{2} service.').format(apply_rule,
|
||||||
|
self.policy_files[self.service],
|
||||||
|
self.service)
|
||||||
LOG.debug(message)
|
LOG.debug(message)
|
||||||
raise rbac_exceptions.RbacParsingException(message)
|
raise rbac_exceptions.RbacParsingException(message)
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -270,9 +270,9 @@ class PolicyAuthorityTest(base.TestCase):
|
||||||
|
|
||||||
fake_rule = 'fake_rule'
|
fake_rule = 'fake_rule'
|
||||||
expected_message = (
|
expected_message = (
|
||||||
"Policy action \"{0}\" not found in policy file: {1} or among "
|
'Policy action "{0}" not found in policy files: {1} or among '
|
||||||
"registered policy in code defaults for service.").format(
|
'registered policy in code defaults for {2} service.').format(
|
||||||
fake_rule, self.custom_policy_file)
|
fake_rule, [self.custom_policy_file], "custom_rbac_policy")
|
||||||
|
|
||||||
e = self.assertRaises(rbac_exceptions.RbacParsingException,
|
e = self.assertRaises(rbac_exceptions.RbacParsingException,
|
||||||
authority.allowed, fake_rule, None)
|
authority.allowed, fake_rule, None)
|
||||||
|
@ -292,9 +292,10 @@ class PolicyAuthorityTest(base.TestCase):
|
||||||
mock.sentinel.error)})
|
mock.sentinel.error)})
|
||||||
|
|
||||||
expected_message = (
|
expected_message = (
|
||||||
"Policy action \"{0}\" not found in policy file: {1} or among "
|
'Policy action "{0}" not found in policy files: {1} or among '
|
||||||
"registered policy in code defaults for service.").format(
|
'registered policy in code defaults for {2} service.').format(
|
||||||
mock.sentinel.rule, self.custom_policy_file)
|
mock.sentinel.rule, [self.custom_policy_file],
|
||||||
|
"custom_rbac_policy")
|
||||||
|
|
||||||
e = self.assertRaises(rbac_exceptions.RbacParsingException,
|
e = self.assertRaises(rbac_exceptions.RbacParsingException,
|
||||||
authority.allowed, mock.sentinel.rule, None)
|
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 = 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_stevedore.named.NamedExtensionManager.return_value = [
|
||||||
mock_manager
|
mock_manager
|
||||||
]
|
]
|
||||||
|
@ -323,7 +324,7 @@ class PolicyAuthorityTest(base.TestCase):
|
||||||
authority = policy_authority.PolicyAuthority(
|
authority = policy_authority.PolicyAuthority(
|
||||||
test_tenant_id, test_user_id, "tenant_rbac_policy")
|
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)
|
self.assertIsInstance(policy_data, str)
|
||||||
|
|
||||||
actual_policy_data = json.loads(policy_data)
|
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 = 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_stevedore.named.NamedExtensionManager.return_value = [
|
||||||
mock_manager
|
mock_manager
|
||||||
]
|
]
|
||||||
|
@ -364,7 +365,7 @@ class PolicyAuthorityTest(base.TestCase):
|
||||||
|
|
||||||
authority = policy_authority.PolicyAuthority(
|
authority = policy_authority.PolicyAuthority(
|
||||||
test_tenant_id, test_user_id, 'tenant_rbac_policy')
|
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)
|
self.assertIsInstance(policy_data, str)
|
||||||
|
|
||||||
actual_policy_data = json.loads(policy_data)
|
actual_policy_data = json.loads(policy_data)
|
||||||
|
@ -388,7 +389,7 @@ class PolicyAuthorityTest(base.TestCase):
|
||||||
None, None, 'test_service')
|
None, None, 'test_service')
|
||||||
|
|
||||||
expected_error = (
|
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}.'
|
'in-code policies or in any of the possible policy files: {1}.'
|
||||||
.format('test_service',
|
.format('test_service',
|
||||||
[CONF.patrole.custom_policy_files[0] % 'test_service']))
|
[CONF.patrole.custom_policy_files[0] % 'test_service']))
|
||||||
|
@ -413,7 +414,7 @@ class PolicyAuthorityTest(base.TestCase):
|
||||||
policy_authority.PolicyAuthority,
|
policy_authority.PolicyAuthority,
|
||||||
None, None, 'test_service')
|
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")
|
.format("test_service")
|
||||||
self.assertIn(expected_error, str(e))
|
self.assertIn(expected_error, str(e))
|
||||||
|
|
||||||
|
@ -435,7 +436,7 @@ class PolicyAuthorityTest(base.TestCase):
|
||||||
None, None, 'tenant_rbac_policy')
|
None, None, 'tenant_rbac_policy')
|
||||||
|
|
||||||
expected_error = (
|
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}.'
|
'in-code policies or in any of the possible policy files: {1}.'
|
||||||
.format('tenant_rbac_policy', [CONF.patrole.custom_policy_files[0]
|
.format('tenant_rbac_policy', [CONF.patrole.custom_policy_files[0]
|
||||||
% 'tenant_rbac_policy']))
|
% 'tenant_rbac_policy']))
|
||||||
|
@ -450,7 +451,7 @@ class PolicyAuthorityTest(base.TestCase):
|
||||||
dir(policy_authority.PolicyAuthority))
|
dir(policy_authority.PolicyAuthority))
|
||||||
self.assertIn('policy_files', dir(policy_parser))
|
self.assertIn('policy_files', dir(policy_parser))
|
||||||
self.assertIn('tenant_rbac_policy', policy_parser.policy_files)
|
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'])
|
policy_parser.policy_files['tenant_rbac_policy'])
|
||||||
|
|
||||||
@mock.patch.object(policy_authority, 'policy', autospec=True)
|
@mock.patch.object(policy_authority, 'policy', autospec=True)
|
||||||
|
@ -458,35 +459,40 @@ class PolicyAuthorityTest(base.TestCase):
|
||||||
autospec=True)
|
autospec=True)
|
||||||
@mock.patch.object(policy_authority, 'clients', autospec=True)
|
@mock.patch.object(policy_authority, 'clients', autospec=True)
|
||||||
@mock.patch.object(policy_authority, 'os', autospec=True)
|
@mock.patch.object(policy_authority, 'os', autospec=True)
|
||||||
def test_discover_policy_files_with_many_invalid_one_valid(self, m_os,
|
@mock.patch.object(policy_authority, 'glob', autospec=True)
|
||||||
m_creds, *args):
|
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.
|
# 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`.
|
# Ensure the outer for loop runs only once in `discover_policy_files`.
|
||||||
m_creds.Manager().identity_services_v3_client.\
|
m_creds.Manager().identity_services_v3_client.\
|
||||||
list_services.return_value = {
|
list_services.return_value = {
|
||||||
'services': [{'name': 'test_service'}]}
|
'services': [{'name': service}]}
|
||||||
|
|
||||||
# The expected policy will be 'baz/test_service'.
|
# The expected policy will be 'baz/test_service'.
|
||||||
self.useFixture(fixtures.ConfPatcher(
|
self.useFixture(fixtures.ConfPatcher(
|
||||||
custom_policy_files=['foo/%s', 'bar/%s', 'baz/%s'],
|
custom_policy_files=custom_policy_files,
|
||||||
group='patrole'))
|
group='patrole'))
|
||||||
|
|
||||||
policy_parser = policy_authority.PolicyAuthority(
|
policy_parser = policy_authority.PolicyAuthority(
|
||||||
None, None, 'test_service')
|
None, None, service)
|
||||||
|
|
||||||
# Ensure that "policy_files" is set at class and instance levels.
|
# Ensure that "policy_files" is set at class and instance levels.
|
||||||
self.assertIn('policy_files',
|
self.assertTrue(hasattr(policy_authority.PolicyAuthority,
|
||||||
dir(policy_authority.PolicyAuthority))
|
'policy_files'))
|
||||||
self.assertIn('policy_files', dir(policy_parser))
|
self.assertTrue(hasattr(policy_parser, 'policy_files'))
|
||||||
self.assertIn('test_service', policy_parser.policy_files)
|
self.assertEqual(['baz/%s' % service],
|
||||||
self.assertEqual('baz/test_service',
|
policy_parser.policy_files[service])
|
||||||
policy_parser.policy_files['test_service'])
|
|
||||||
|
|
||||||
def test_discover_policy_files_with_no_valid_files(self):
|
def test_discover_policy_files_with_no_valid_files(self):
|
||||||
expected_error = (
|
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}.'
|
'in-code policies or in any of the possible policy files: {1}.'
|
||||||
.format('test_service', [self.conf_policy_path % 'test_service']))
|
.format('test_service', [self.conf_policy_path % 'test_service']))
|
||||||
|
|
||||||
|
@ -495,11 +501,11 @@ class PolicyAuthorityTest(base.TestCase):
|
||||||
None, None, 'test_service')
|
None, None, 'test_service')
|
||||||
self.assertIn(expected_error, str(e))
|
self.assertIn(expected_error, str(e))
|
||||||
|
|
||||||
self.assertIn('policy_files',
|
self.assertTrue(hasattr(policy_authority.PolicyAuthority,
|
||||||
dir(policy_authority.PolicyAuthority))
|
'policy_files'))
|
||||||
self.assertNotIn(
|
self.assertEqual(
|
||||||
'test_service',
|
[],
|
||||||
policy_authority.PolicyAuthority.policy_files.keys())
|
policy_authority.PolicyAuthority.policy_files['test_service'])
|
||||||
|
|
||||||
def _test_validate_service(self, v2_services, v3_services,
|
def _test_validate_service(self, v2_services, v3_services,
|
||||||
expected_failure=False, expected_services=None):
|
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