Add helper scripts for generating policy info
This adds two helper scripts that consuming projects can use to get information that helps deployers. The oslopolicy-policy-generator script looks at an entry_point for a configured policy.Enforcer and outputs a yaml formatted policy file for that configuration. This is a merge of registered rules and configured rules. The oslopolicy_list_redundant script looks at an entry_point for a configured policy.Enforcer and outputs a yaml formatted policy file with a list of policies where the registered default matches the project configuration. These are policies that can be removed from the configuration file(s) without affecting policy. Change-Id: Ibe4e6c9288768bcc8f532e384524580c57e58275 Implements: bp policy-sample-generation
This commit is contained in:
parent
474c120ae6
commit
85ebe9eb5f
@ -52,10 +52,16 @@ benefits.
|
|||||||
policies used are registered. The signature of Enforcer.authorize matches
|
policies used are registered. The signature of Enforcer.authorize matches
|
||||||
Enforcer.enforce.
|
Enforcer.enforce.
|
||||||
|
|
||||||
* More will be documented as capabilities are added.
|
|
||||||
* A sample policy file can be generated based on the registered policies
|
* A sample policy file can be generated based on the registered policies
|
||||||
rather than needing to manually maintain one.
|
rather than needing to manually maintain one.
|
||||||
|
|
||||||
|
* A policy file can be generated which is a merge of registered defaults and
|
||||||
|
policies loaded from a file. This shows the effective policy in use.
|
||||||
|
|
||||||
|
* A list can be generated which contains policies defined in a file which match
|
||||||
|
defaults registered in code. These are candidates for removal from the file
|
||||||
|
in order to keep it small and understandable.
|
||||||
|
|
||||||
How to register
|
How to register
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
@ -106,3 +112,71 @@ where policy-generator.conf looks like::
|
|||||||
namespace = nova.compute.api
|
namespace = nova.compute.api
|
||||||
|
|
||||||
If output_file is ommitted the sample file will be sent to stdout.
|
If output_file is ommitted the sample file will be sent to stdout.
|
||||||
|
|
||||||
|
Merged file generation
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
This will output a policy file which includes all registered policy defaults
|
||||||
|
and all policies configured with a policy file. This file shows the effective
|
||||||
|
policy in use by the project.
|
||||||
|
|
||||||
|
In setup.cfg of a project using oslo.policy::
|
||||||
|
|
||||||
|
[entry_points]
|
||||||
|
oslo.policy.enforcer =
|
||||||
|
nova = nova.policy:get_enforcer
|
||||||
|
|
||||||
|
where get_enforcer is a method that returns a configured
|
||||||
|
oslo_policy.policy.Enforcer object. This object should be setup exactly as it
|
||||||
|
is used for actual policy enforcement, if it differs the generated policy file
|
||||||
|
may not match reality.
|
||||||
|
|
||||||
|
Run the oslopolicy-policy-generator script with some configuration options::
|
||||||
|
|
||||||
|
oslopolicy-policy-generator --namespace nova --output-file policy-merged.yaml
|
||||||
|
|
||||||
|
or::
|
||||||
|
|
||||||
|
oslopolicy-policy-generator --config-file policy-merged-generator.conf
|
||||||
|
|
||||||
|
where policy-merged-generator.conf looks like::
|
||||||
|
|
||||||
|
[DEFAULT]
|
||||||
|
output_file = policy-merged.yaml
|
||||||
|
namespace = nova
|
||||||
|
|
||||||
|
If output_file is ommitted the file will be sent to stdout.
|
||||||
|
|
||||||
|
List of redundant configuration
|
||||||
|
-------------------------------
|
||||||
|
|
||||||
|
This will output a list of matches for policy rules that are defined in a
|
||||||
|
configuration file where the rule does not differ from a registered default
|
||||||
|
rule. These are rules that can be removed from the policy file with no change
|
||||||
|
in effective policy.
|
||||||
|
|
||||||
|
In setup.cfg of a project using oslo.policy::
|
||||||
|
|
||||||
|
[entry_points]
|
||||||
|
oslo.policy.enforcer =
|
||||||
|
nova = nova.policy:get_enforcer
|
||||||
|
|
||||||
|
where get_enforcer is a method that returns a configured
|
||||||
|
oslo_policy.policy.Enforcer object. This object should be setup exactly as it
|
||||||
|
is used for actual policy enforcement, if it differs the generated policy file
|
||||||
|
may not match reality.
|
||||||
|
|
||||||
|
Run the oslopolicy-list-redundant script::
|
||||||
|
|
||||||
|
oslopolicy-list-redundant --namespace nova
|
||||||
|
|
||||||
|
or::
|
||||||
|
|
||||||
|
oslopolicy-list-redundant --config-file policy-redundant.conf
|
||||||
|
|
||||||
|
where policy-redundant.conf looks like::
|
||||||
|
|
||||||
|
[DEFAULT]
|
||||||
|
namespace = nova
|
||||||
|
|
||||||
|
Output will go to stdout.
|
||||||
|
@ -17,17 +17,29 @@ import textwrap
|
|||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
import stevedore
|
import stevedore
|
||||||
|
|
||||||
|
from oslo_policy import policy
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
_generator_opts = [
|
_generator_opts = [
|
||||||
cfg.StrOpt('output-file',
|
cfg.StrOpt('output-file',
|
||||||
help='Path of the file to write to. Defaults to stdout.'),
|
help='Path of the file to write to. Defaults to stdout.'),
|
||||||
|
]
|
||||||
|
|
||||||
|
_rule_opts = [
|
||||||
cfg.MultiStrOpt('namespace',
|
cfg.MultiStrOpt('namespace',
|
||||||
required=True,
|
required=True,
|
||||||
help='Option namespace(s) under "oslo.policy.policies" in '
|
help='Option namespace(s) under "oslo.policy.policies" in '
|
||||||
'which to query for options.'),
|
'which to query for options.'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
_enforcer_opts = [
|
||||||
|
cfg.StrOpt('namespace',
|
||||||
|
required=True,
|
||||||
|
help='Option namespace under "oslo.policy.enforcer" in '
|
||||||
|
'which to look for a policy.Enforcer.'),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def _get_policies_dict(namespaces):
|
def _get_policies_dict(namespaces):
|
||||||
"""Find the options available via the given namespaces.
|
"""Find the options available via the given namespaces.
|
||||||
@ -47,6 +59,23 @@ def _get_policies_dict(namespaces):
|
|||||||
return opts
|
return opts
|
||||||
|
|
||||||
|
|
||||||
|
def _get_enforcer(namespace):
|
||||||
|
"""Find a policy.Enforcer via an entry point with the given namespace.
|
||||||
|
|
||||||
|
:param namespace: a namespace under oslo.policy.enforcer where the desired
|
||||||
|
enforcer object can be found.
|
||||||
|
:returns: a policy.Enforcer object
|
||||||
|
"""
|
||||||
|
mgr = stevedore.named.NamedExtensionManager(
|
||||||
|
'oslo.policy.enforcer',
|
||||||
|
names=[namespace],
|
||||||
|
on_load_failure_callback=on_load_failure_callback,
|
||||||
|
invoke_on_load=True)
|
||||||
|
enforcer = mgr[namespace].obj
|
||||||
|
|
||||||
|
return enforcer
|
||||||
|
|
||||||
|
|
||||||
def _format_help_text(description):
|
def _format_help_text(description):
|
||||||
"""Format a comment for a policy based on the description provided.
|
"""Format a comment for a policy based on the description provided.
|
||||||
|
|
||||||
@ -117,6 +146,51 @@ def _generate_sample(namespaces, output_file=None):
|
|||||||
output_file.write(section)
|
output_file.write(section)
|
||||||
|
|
||||||
|
|
||||||
|
def _generate_policy(namespace, output_file=None):
|
||||||
|
"""Generate a policy file showing what will be used.
|
||||||
|
|
||||||
|
This takes all registered policies and merges them with what's defined in
|
||||||
|
a policy file and outputs the result. That result is the effective policy
|
||||||
|
that will be honored by policy checks.
|
||||||
|
|
||||||
|
:param output_file: The path of a file to output to. stdout used if None.
|
||||||
|
"""
|
||||||
|
enforcer = _get_enforcer(namespace)
|
||||||
|
# Ensure that files have been parsed
|
||||||
|
enforcer.load_rules()
|
||||||
|
|
||||||
|
file_rules = [policy.RuleDefault(name, default.check_str)
|
||||||
|
for name, default in enforcer.file_rules.items()]
|
||||||
|
registered_rules = [policy.RuleDefault(name, default.check_str)
|
||||||
|
for name, default in enforcer.registered_rules.items()
|
||||||
|
if name not in enforcer.file_rules]
|
||||||
|
policies = {'rules': file_rules + registered_rules}
|
||||||
|
|
||||||
|
output_file = (open(output_file, 'w') if output_file
|
||||||
|
else sys.stdout)
|
||||||
|
|
||||||
|
for section in _sort_and_format_by_section(policies, include_help=False):
|
||||||
|
output_file.write(section)
|
||||||
|
|
||||||
|
|
||||||
|
def _list_redundant(namespace):
|
||||||
|
"""Generate a list of configured policies which match defaults.
|
||||||
|
|
||||||
|
This checks all policies loaded from policy files and checks to see if they
|
||||||
|
match registered policies. If so then it is redundant to have them defined
|
||||||
|
in a policy file and operators should consider removing them.
|
||||||
|
"""
|
||||||
|
enforcer = _get_enforcer(namespace)
|
||||||
|
# Ensure that files have been parsed
|
||||||
|
enforcer.load_rules()
|
||||||
|
|
||||||
|
for name, file_rule in enforcer.file_rules.items():
|
||||||
|
reg_rule = enforcer.registered_rules.get(name, None)
|
||||||
|
if reg_rule:
|
||||||
|
if file_rule == reg_rule:
|
||||||
|
print(reg_rule)
|
||||||
|
|
||||||
|
|
||||||
def on_load_failure_callback(*args, **kwargs):
|
def on_load_failure_callback(*args, **kwargs):
|
||||||
raise
|
raise
|
||||||
|
|
||||||
@ -124,7 +198,25 @@ def on_load_failure_callback(*args, **kwargs):
|
|||||||
def generate_sample(args=None):
|
def generate_sample(args=None):
|
||||||
logging.basicConfig(level=logging.WARN)
|
logging.basicConfig(level=logging.WARN)
|
||||||
conf = cfg.ConfigOpts()
|
conf = cfg.ConfigOpts()
|
||||||
conf.register_cli_opts(_generator_opts)
|
conf.register_cli_opts(_generator_opts + _rule_opts)
|
||||||
conf.register_opts(_generator_opts)
|
conf.register_opts(_generator_opts + _rule_opts)
|
||||||
conf(args)
|
conf(args)
|
||||||
_generate_sample(conf.namespace, conf.output_file)
|
_generate_sample(conf.namespace, conf.output_file)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_policy(args=None):
|
||||||
|
logging.basicConfig(level=logging.WARN)
|
||||||
|
conf = cfg.ConfigOpts()
|
||||||
|
conf.register_cli_opts(_generator_opts + _enforcer_opts)
|
||||||
|
conf.register_opts(_generator_opts + _enforcer_opts)
|
||||||
|
conf(args)
|
||||||
|
_generate_policy(conf.namespace, conf.output_file)
|
||||||
|
|
||||||
|
|
||||||
|
def list_redundant(args=None):
|
||||||
|
logging.basicConfig(level=logging.WARN)
|
||||||
|
conf = cfg.ConfigOpts()
|
||||||
|
conf.register_cli_opts(_enforcer_opts)
|
||||||
|
conf.register_opts(_enforcer_opts)
|
||||||
|
conf(args)
|
||||||
|
_list_redundant(conf.namespace)
|
||||||
|
@ -9,12 +9,14 @@
|
|||||||
# 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 operator
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
import fixtures
|
import fixtures
|
||||||
import mock
|
import mock
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
from six import moves
|
from six import moves
|
||||||
|
import stevedore
|
||||||
import testtools
|
import testtools
|
||||||
|
|
||||||
from oslo_policy import generator
|
from oslo_policy import generator
|
||||||
@ -148,3 +150,118 @@ class GeneratorRaiseErrorTestCase(testtools.TestCase):
|
|||||||
with mock.patch('sys.argv', testargs):
|
with mock.patch('sys.argv', testargs):
|
||||||
self.assertRaises(cfg.RequiredOptError, generator.generate_sample,
|
self.assertRaises(cfg.RequiredOptError, generator.generate_sample,
|
||||||
[])
|
[])
|
||||||
|
|
||||||
|
|
||||||
|
class GeneratePolicyTestCase(base.PolicyBaseTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
super(GeneratePolicyTestCase, self).setUp()
|
||||||
|
|
||||||
|
def test_merged_rules(self):
|
||||||
|
extensions = []
|
||||||
|
for name, opts in OPTS.items():
|
||||||
|
ext = stevedore.extension.Extension(name=name, entry_point=None,
|
||||||
|
plugin=None, obj=opts)
|
||||||
|
extensions.append(ext)
|
||||||
|
test_mgr = stevedore.named.NamedExtensionManager.make_test_instance(
|
||||||
|
extensions=extensions, namespace=['base_rules', 'rules'])
|
||||||
|
|
||||||
|
# Write the policy file for an enforcer to load
|
||||||
|
sample_file = self.get_config_file_fullname('policy-sample.yaml')
|
||||||
|
with mock.patch('stevedore.named.NamedExtensionManager',
|
||||||
|
return_value=test_mgr):
|
||||||
|
generator._generate_sample(['base_rules', 'rules'], sample_file)
|
||||||
|
|
||||||
|
enforcer = policy.Enforcer(self.conf, policy_file='policy-sample.yaml')
|
||||||
|
# register an opt defined in the file
|
||||||
|
enforcer.register_default(policy.RuleDefault('admin',
|
||||||
|
'is_admin:False'))
|
||||||
|
# register a new opt
|
||||||
|
enforcer.register_default(policy.RuleDefault('foo', 'role:foo'))
|
||||||
|
|
||||||
|
# Mock out stevedore to return the configured enforcer
|
||||||
|
ext = stevedore.extension.Extension(name='testing', entry_point=None,
|
||||||
|
plugin=None, obj=enforcer)
|
||||||
|
test_mgr = stevedore.named.NamedExtensionManager.make_test_instance(
|
||||||
|
extensions=[ext], namespace='testing')
|
||||||
|
|
||||||
|
# Generate a merged file
|
||||||
|
merged_file = self.get_config_file_fullname('policy-merged.yaml')
|
||||||
|
with mock.patch('stevedore.named.NamedExtensionManager',
|
||||||
|
return_value=test_mgr) as mock_ext_mgr:
|
||||||
|
generator._generate_policy(namespace='testing',
|
||||||
|
output_file=merged_file)
|
||||||
|
mock_ext_mgr.assert_called_once_with(
|
||||||
|
'oslo.policy.enforcer', names=['testing'],
|
||||||
|
on_load_failure_callback=generator.on_load_failure_callback,
|
||||||
|
invoke_on_load=True)
|
||||||
|
|
||||||
|
# load the merged file with a new enforcer
|
||||||
|
merged_enforcer = policy.Enforcer(self.conf,
|
||||||
|
policy_file='policy-merged.yaml')
|
||||||
|
merged_enforcer.load_rules()
|
||||||
|
for rule in ['admin', 'owner', 'admin_or_owner', 'foo']:
|
||||||
|
self.assertIn(rule, merged_enforcer.rules)
|
||||||
|
|
||||||
|
self.assertEqual('is_admin:True', str(merged_enforcer.rules['admin']))
|
||||||
|
self.assertEqual('role:foo', str(merged_enforcer.rules['foo']))
|
||||||
|
|
||||||
|
|
||||||
|
class ListRedundantTestCase(base.PolicyBaseTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
super(ListRedundantTestCase, self).setUp()
|
||||||
|
|
||||||
|
def _capture_stdout(self):
|
||||||
|
self.useFixture(fixtures.MonkeyPatch('sys.stdout', moves.StringIO()))
|
||||||
|
return sys.stdout
|
||||||
|
|
||||||
|
def test_matched_rules(self):
|
||||||
|
extensions = []
|
||||||
|
for name, opts in OPTS.items():
|
||||||
|
ext = stevedore.extension.Extension(name=name, entry_point=None,
|
||||||
|
plugin=None, obj=opts)
|
||||||
|
extensions.append(ext)
|
||||||
|
test_mgr = stevedore.named.NamedExtensionManager.make_test_instance(
|
||||||
|
extensions=extensions, namespace=['base_rules', 'rules'])
|
||||||
|
|
||||||
|
# Write the policy file for an enforcer to load
|
||||||
|
sample_file = self.get_config_file_fullname('policy-sample.yaml')
|
||||||
|
with mock.patch('stevedore.named.NamedExtensionManager',
|
||||||
|
return_value=test_mgr):
|
||||||
|
generator._generate_sample(['base_rules', 'rules'], sample_file)
|
||||||
|
|
||||||
|
enforcer = policy.Enforcer(self.conf, policy_file='policy-sample.yaml')
|
||||||
|
# register opts that match those defined in policy-sample.yaml
|
||||||
|
enforcer.register_default(policy.RuleDefault('admin', 'is_admin:True'))
|
||||||
|
enforcer.register_default(
|
||||||
|
policy.RuleDefault('owner', 'project_id:%(project_id)s'))
|
||||||
|
# register a new opt
|
||||||
|
enforcer.register_default(policy.RuleDefault('foo', 'role:foo'))
|
||||||
|
|
||||||
|
# Mock out stevedore to return the configured enforcer
|
||||||
|
ext = stevedore.extension.Extension(name='testing', entry_point=None,
|
||||||
|
plugin=None, obj=enforcer)
|
||||||
|
test_mgr = stevedore.named.NamedExtensionManager.make_test_instance(
|
||||||
|
extensions=[ext], namespace='testing')
|
||||||
|
|
||||||
|
stdout = self._capture_stdout()
|
||||||
|
with mock.patch('stevedore.named.NamedExtensionManager',
|
||||||
|
return_value=test_mgr) as mock_ext_mgr:
|
||||||
|
generator._list_redundant(namespace='testing')
|
||||||
|
mock_ext_mgr.assert_called_once_with(
|
||||||
|
'oslo.policy.enforcer', names=['testing'],
|
||||||
|
on_load_failure_callback=generator.on_load_failure_callback,
|
||||||
|
invoke_on_load=True)
|
||||||
|
|
||||||
|
matches = [line.split(': ', 1) for
|
||||||
|
line in stdout.getvalue().splitlines()]
|
||||||
|
matches.sort(key=operator.itemgetter(0))
|
||||||
|
|
||||||
|
# Should be 'admin'
|
||||||
|
opt0 = matches[0]
|
||||||
|
self.assertEqual('"admin"', opt0[0])
|
||||||
|
self.assertEqual('"is_admin:True"', opt0[1])
|
||||||
|
|
||||||
|
# Should be 'owner'
|
||||||
|
opt1 = matches[1]
|
||||||
|
self.assertEqual('"owner"', opt1[0])
|
||||||
|
self.assertEqual('"project_id:%(project_id)s"', opt1[1])
|
||||||
|
@ -33,6 +33,8 @@ oslo.config.opts =
|
|||||||
console_scripts =
|
console_scripts =
|
||||||
oslopolicy-checker = oslo_policy.shell:main
|
oslopolicy-checker = oslo_policy.shell:main
|
||||||
oslopolicy-sample-generator = oslo_policy.generator:generate_sample
|
oslopolicy-sample-generator = oslo_policy.generator:generate_sample
|
||||||
|
oslopolicy-policy-generator = oslo_policy.generator:genarate_policy
|
||||||
|
oslopolicy-list-redundant = oslo_policy.generator:list_redundant
|
||||||
|
|
||||||
[build_sphinx]
|
[build_sphinx]
|
||||||
source-dir = doc/source
|
source-dir = doc/source
|
||||||
|
Loading…
x
Reference in New Issue
Block a user