diff options
author | Zuul <zuul@review.openstack.org> | 2018-12-04 18:11:32 +0000 |
---|---|---|
committer | Gerrit Code Review <review@openstack.org> | 2018-12-04 18:11:32 +0000 |
commit | 0af23a37bc2e6f30e5b24ffb47ef876d7a2b7c7a (patch) | |
tree | 0a5df69c8ee1611dfe132b510fa3b13a4343ac31 /oslo_policy | |
parent | f1505dd4071d9b73c21be683db4c9911ea8f8058 (diff) | |
parent | 5aeac664ae38f5045a32d39fdc5162fa9623576d (diff) | |
download | oslo-policy-0af23a37bc2e6f30e5b24ffb47ef876d7a2b7c7a.tar.gz |
Merge "Make upgrades more robust with policy overrides"
Diffstat (limited to 'oslo_policy')
-rw-r--r-- | oslo_policy/policy.py | 146 | ||||
-rw-r--r-- | oslo_policy/tests/test_policy.py | 118 |
2 files changed, 204 insertions, 60 deletions
diff --git a/oslo_policy/policy.py b/oslo_policy/policy.py index fdd48bb..fe430bc 100644 --- a/oslo_policy/policy.py +++ b/oslo_policy/policy.py @@ -575,66 +575,11 @@ class Enforcer(object): force_reload, False) for default in self.registered_rules.values(): - if default.deprecated_rule: - deprecated_msg = ( - 'Policy "%(old_name)s":"%(old_check_str)s" was ' - 'deprecated in %(release)s in favor of "%(name)s":' - '"%(check_str)s". Reason: %(reason)s. Either ensure ' - 'your deployment is ready for the new default or ' - 'copy/paste the deprecated policy into your policy ' - 'file and maintain it manually.' % { - 'old_name': default.deprecated_rule.name, - 'old_check_str': default.deprecated_rule.check_str, - 'release': default.deprecated_since, - 'name': default.name, - 'check_str': default.check_str, - 'reason': default.deprecated_reason - } - ) - if default.deprecated_rule.name != default.name and ( - default.deprecated_rule.name in self.rules): - # Print a warning because the actual policy name is - # changing. If deployers are relying on an override for - # foo:bar and it's getting renamed to foo:create_bar - # then they need to be able to see that before they - # roll out the next release. - warnings.warn(deprecated_msg) - if (default.deprecated_rule.check_str != - default.check_str and default.name not in - self.rules): - # In this case, the default check_str is changing. We - # need to let operators know that this is going to - # change. If they don't want to override it, they are - # going to have to make sure the right infrastructure - # exists before they upgrade. This overrides the new - # check with an OrCheck that combines the new and old - # check_str attributes from the new and deprecated - # policies. This will make it so that deployments don't - # break on upgrade, but they receive log messages - # telling them stuff is going to change if they don't - # maintain the policy manually or add infrastructure to - # their deployment to support the new policy. - default.check = _parser.parse_rule( - default.check_str + ' or ' + - default.deprecated_rule.check_str - ) - warnings.warn(deprecated_msg) - if default.deprecated_for_removal and ( - default.name in self.file_rules): - # If a policy is going to be removed altogether, then we - # need to make sure we let operators know so they can clean - # up their policy files, if they are overriding it. - warnings.warn( - 'Policy "%(policy)s":"%(check_str)s" was ' - 'deprecated for removal in %(release)s. Reason: ' - '%(reason)s. Its value may be silently ignored in ' - 'the future.' % { - 'policy': default.name, - 'check_str': default.check_str, - 'release': default.deprecated_since, - 'reason': default.deprecated_reason - } - ) + if default.deprecated_for_removal: + self._emit_deprecated_for_removal_warning(default) + elif default.deprecated_rule: + self._handle_deprecated_rule(default) + if default.name not in self.rules: self.rules[default.name] = default.check @@ -668,6 +613,87 @@ class Enforcer(object): return not violation + def _emit_deprecated_for_removal_warning(self, default): + # If the policy is being removed completely, we need to let operators + # know that the policy is going to be silently ignored in the future + # and they can remove it from their overrides since it isn't being + # replaced by another policy. + if default.name in self.file_rules: + warnings.warn( + 'Policy "%(policy)s":"%(check_str)s" was deprecated for ' + 'removal in %(release)s. Reason: %(reason)s. Its value may be ' + 'silently ignored in the future.' % { + 'policy': default.name, + 'check_str': default.check_str, + 'release': default.deprecated_since, + 'reason': default.deprecated_reason + } + ) + + def _handle_deprecated_rule(self, default): + """Handle cases where a policy rule has been deprecated. + + :param default: an instance of RuleDefault that contains an instance of + DeprecatedRule + """ + + deprecated_rule = default.deprecated_rule + + deprecated_msg = ( + 'Policy "%(old_name)s":"%(old_check_str)s" was deprecated in ' + '%(release)s in favor of "%(name)s":"%(check_str)s". Reason: ' + '%(reason)s. Either ensure your deployment is ready for the new ' + 'default or copy/paste the deprecated policy into your policy ' + 'file and maintain it manually.' % { + 'old_name': deprecated_rule.name, + 'old_check_str': deprecated_rule.check_str, + 'release': default.deprecated_since, + 'name': default.name, + 'check_str': default.check_str, + 'reason': default.deprecated_reason + } + ) + + # Print a warning because the actual policy name is changing. If + # operators are relying on an override for foo:bar and it's getting + # renamed to foo:create_bar then they need to be able to see that + # before they roll out the next release. If the policy name is in + # self.file_rules, we know that it's being overridden. + if deprecated_rule.name != default.name and ( + deprecated_rule.name in self.file_rules): + + warnings.warn(deprecated_msg) + + # If the deprecated policy is being overridden and doesn't match + # the default deprecated policy, override the new policy's default + # with the old check string. This should prevents unwanted exposure + # to APIs on upgrade. + if (self.file_rules[deprecated_rule.name].check + != _parser.parse_rule(deprecated_rule.check_str)): + if default.name not in self.file_rules.keys(): + self.rules[default.name] = self.file_rules[ + deprecated_rule.name + ].check + + # In this case, the default check string is changing. We need to let + # operators know that this is going to change. If they don't want to + # override it, they are going to have to make sure the right + # infrastructure exists before they upgrade. This overrides the new + # check with an OrCheck that combines the new and old check_str + # attributes from the new and deprecated policies. This will make it so + # that deployments don't break on upgrade, but they receive log + # messages telling them stuff is going to change if they don't maintain + # the policy manually or add infrastructure to their deployment to + # support the new policy. + if (deprecated_rule.check_str != default.check_str + and default.name not in self.file_rules): + + default.check = _parser.parse_rule( + default.check_str + ' or ' + + deprecated_rule.check_str + ) + warnings.warn(deprecated_msg) + def _undefined_check(self, check): '''Check if a RuleCheck references an undefined rule.''' if isinstance(check, RuleCheck): diff --git a/oslo_policy/tests/test_policy.py b/oslo_policy/tests/test_policy.py index 2285381..8eaca5e 100644 --- a/oslo_policy/tests/test_policy.py +++ b/oslo_policy/tests/test_policy.py @@ -1235,6 +1235,124 @@ class DocumentedRuleDefaultDeprecationTestCase(base.PolicyBaseTestCase): deprecated_since='N' ) + def test_override_deprecated_policy_with_old_name(self): + # Simulate an operator overriding a policy + rules = jsonutils.dumps({'foo:bar': 'role:bazz'}) + self.create_config_file('policy.json', rules) + + # Deprecate the policy name and check string in favor of something + # better. + deprecated_rule = policy.DeprecatedRule( + name='foo:bar', + check_str='role:fizz' + ) + rule_list = [policy.DocumentedRuleDefault( + name='foo:create_bar', + check_str='role:bang', + description='Create a bar.', + operations=[{'path': '/v1/bars', 'method': 'POST'}], + deprecated_rule=deprecated_rule, + deprecated_reason='"role:bang" is a better default', + deprecated_since='N' + )] + self.enforcer.register_defaults(rule_list) + + # Make sure the override supplied by the operator using the old policy + # name is used in favor of the old or new default. + self.assertFalse( + self.enforcer.enforce('foo:create_bar', {}, {'roles': ['fizz']}) + ) + self.assertFalse( + self.enforcer.enforce('foo:create_bar', {}, {'roles': ['bang']}) + ) + self.assertTrue( + self.enforcer.enforce('foo:create_bar', {}, {'roles': ['bazz']}) + ) + + def test_override_deprecated_policy_with_new_name(self): + # Simulate an operator overriding a policy using the new policy name + rules = jsonutils.dumps({'foo:create_bar': 'role:bazz'}) + self.create_config_file('policy.json', rules) + + # Deprecate the policy name and check string in favor of something + # better. + deprecated_rule = policy.DeprecatedRule( + name='foo:bar', + check_str='role:fizz' + ) + rule_list = [policy.DocumentedRuleDefault( + name='foo:create_bar', + check_str='role:bang', + description='Create a bar.', + operations=[{'path': '/v1/bars', 'method': 'POST'}], + deprecated_rule=deprecated_rule, + deprecated_reason='"role:bang" is a better default', + deprecated_since='N' + )] + self.enforcer.register_defaults(rule_list) + + # Make sure the override supplied by the operator is being used in + # place of either default value. + self.assertFalse( + self.enforcer.enforce('foo:create_bar', {}, {'roles': ['fizz']}) + ) + self.assertFalse( + self.enforcer.enforce('foo:create_bar', {}, {'roles': ['bang']}) + ) + self.assertTrue( + self.enforcer.enforce('foo:create_bar', {}, {'roles': ['bazz']}) + ) + + def test_override_both_new_and_old_policy(self): + # Simulate an operator overriding a policy using both the the new and + # old policy names. The following doesn't make a whole lot of sense + # because the overrides are conflicting, but we want to make sure that + # oslo.policy uses foo:create_bar instead of foo:bar. + rules_dict = { + 'foo:create_bar': 'role:bazz', + 'foo:bar': 'role:wee' + } + rules = jsonutils.dumps(rules_dict) + self.create_config_file('policy.json', rules) + + # Deprecate the policy name and check string in favor of something + # better. + deprecated_rule = policy.DeprecatedRule( + name='foo:bar', + check_str='role:fizz' + ) + rule_list = [policy.DocumentedRuleDefault( + name='foo:create_bar', + check_str='role:bang', + description='Create a bar.', + operations=[{'path': '/v1/bars', 'method': 'POST'}], + deprecated_rule=deprecated_rule, + deprecated_reason='"role:bang" is a better default', + deprecated_since='N' + )] + self.enforcer.register_defaults(rule_list) + + # The default check string for the old policy name foo:bar should fail + self.assertFalse( + self.enforcer.enforce('foo:create_bar', {}, {'roles': ['fizz']}) + ) + + # The default check string for the new policy name foo:create_bar + # should fail + self.assertFalse( + self.enforcer.enforce('foo:create_bar', {}, {'roles': ['bang']}) + ) + + # The override for the old policy name foo:bar should fail + self.assertFalse( + self.enforcer.enforce('foo:create_bar', {}, {'roles': ['wee']}) + ) + + # The override for foo:create_bar should pass + self.assertTrue( + self.enforcer.enforce('foo:create_bar', {}, {'roles': ['bazz']}) + ) + class DocumentedRuleDefaultTestCase(base.PolicyBaseTestCase): |