diff --git a/.gitignore b/.gitignore index 1117845da2..942e189787 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,9 @@ tests.sqlite glance/versioninfo subunit.log +# generated policy file +etc/policy.yaml.sample + # Swap files range from .saa to .swp *.s[a-w][a-p] diff --git a/etc/glance-policy-generator.conf b/etc/glance-policy-generator.conf new file mode 100644 index 0000000000..947f5fee87 --- /dev/null +++ b/etc/glance-policy-generator.conf @@ -0,0 +1,3 @@ +[DEFAULT] +namespace = glance +output_file = etc/policy.yaml.sample diff --git a/etc/policy.json b/etc/policy.json index 5b1f6be7eb..2c63c08510 100644 --- a/etc/policy.json +++ b/etc/policy.json @@ -1,63 +1,2 @@ { - "context_is_admin": "role:admin", - "default": "role:admin", - - "add_image": "", - "delete_image": "", - "get_image": "", - "get_images": "", - "modify_image": "", - "publicize_image": "role:admin", - "communitize_image": "", - "copy_from": "", - - "download_image": "", - "upload_image": "", - - "delete_image_location": "", - "get_image_location": "", - "set_image_location": "", - - "add_member": "", - "delete_member": "", - "get_member": "", - "get_members": "", - "modify_member": "", - - "manage_image_cache": "role:admin", - - "get_task": "", - "get_tasks": "", - "add_task": "", - "modify_task": "", - "tasks_api_access": "role:admin", - - "deactivate": "", - "reactivate": "", - - "get_metadef_namespace": "", - "get_metadef_namespaces":"", - "modify_metadef_namespace":"", - "add_metadef_namespace":"", - - "get_metadef_object":"", - "get_metadef_objects":"", - "modify_metadef_object":"", - "add_metadef_object":"", - - "list_metadef_resource_types":"", - "get_metadef_resource_type":"", - "add_metadef_resource_type_association":"", - - "get_metadef_property":"", - "get_metadef_properties":"", - "modify_metadef_property":"", - "add_metadef_property":"", - - "get_metadef_tag":"", - "get_metadef_tags":"", - "modify_metadef_tag":"", - "add_metadef_tag":"", - "add_metadef_tags":"" - } diff --git a/glance/api/policy.py b/glance/api/policy.py index 27e92c9a0d..f14dbc1877 100644 --- a/glance/api/policy.py +++ b/glance/api/policy.py @@ -32,34 +32,26 @@ from oslo_policy import policy from glance.common import exception import glance.domain.proxy from glance.i18n import _ +from glance import policies LOG = logging.getLogger(__name__) CONF = cfg.CONF _ENFORCER = None -DEFAULT_RULES = policy.Rules.from_dict({ - 'context_is_admin': 'role:admin', - 'default': 'role:admin', - 'manage_image_cache': 'role:admin', -}) - class Enforcer(policy.Enforcer): """Responsible for loading and enforcing rules""" def __init__(self): - if CONF.find_file(CONF.oslo_policy.policy_file): - kwargs = dict(rules=None, use_conf=True) - else: - kwargs = dict(rules=DEFAULT_RULES, use_conf=False) - super(Enforcer, self).__init__(CONF, overwrite=False, **kwargs) + super(Enforcer, self).__init__(CONF, use_conf=True, overwrite=False) + self.register_defaults(policies.list_rules()) def add_rules(self, rules): """Add new rules to the Rules object""" self.set_rules(rules, overwrite=False, use_conf=self.use_conf) - def enforce(self, context, action, target): + def enforce(self, context, action, target, registered=True): """Verifies that the action is valid on the target in this context. :param context: Glance request context @@ -68,13 +60,15 @@ class Enforcer(policy.Enforcer): :raises: `glance.common.exception.Forbidden` :returns: A non-False value if access is allowed. """ + if registered and action not in self.registered_rules: + raise policy.PolicyNotRegistered(action) return super(Enforcer, self).enforce(action, target, context.to_policy_values(), do_raise=True, exc=exception.Forbidden, action=action) - def check(self, context, action, target): + def check(self, context, action, target, registered=True): """Verifies that the action is valid on the target in this context. :param context: Glance request context @@ -82,6 +76,8 @@ class Enforcer(policy.Enforcer): :param target: Dictionary representing the object of the action. :returns: A non-False value if access is allowed. """ + if registered and action not in self.registered_rules: + raise policy.PolicyNotRegistered(action) return super(Enforcer, self).enforce(action, target, context.to_policy_values()) diff --git a/glance/common/property_utils.py b/glance/common/property_utils.py index 718d464832..21915b2014 100644 --- a/glance/common/property_utils.py +++ b/glance/common/property_utils.py @@ -209,7 +209,7 @@ class PropertyRules(object): def _check_policy(self, property_exp, action, context): try: action = ":".join([property_exp, action]) - self.policy_enforcer.enforce(context, action, {}) + self.policy_enforcer.enforce(context, action, {}, registered=False) except exception.Forbidden: return False return True diff --git a/glance/policies/__init__.py b/glance/policies/__init__.py new file mode 100644 index 0000000000..67b9dfc07b --- /dev/null +++ b/glance/policies/__init__.py @@ -0,0 +1,27 @@ +# 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 itertools + +from glance.policies import base +from glance.policies import image +from glance.policies import metadef +from glance.policies import tasks + + +def list_rules(): + return itertools.chain( + base.list_rules(), + image.list_rules(), + tasks.list_rules(), + metadef.list_rules(), + ) diff --git a/glance/policies/base.py b/glance/policies/base.py new file mode 100644 index 0000000000..00266cf72c --- /dev/null +++ b/glance/policies/base.py @@ -0,0 +1,28 @@ +# 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. + +from oslo_policy import policy + + +rules = [ + policy.RuleDefault(name='default', check_str='', + description='Defines the default rule used for ' + 'policies that historically had an empty ' + 'policy in the supplied policy.json file.'), + policy.RuleDefault(name='context_is_admin', check_str='role:admin', + description='Defines the rule for the is_admin:True ' + 'check.'), +] + + +def list_rules(): + return rules diff --git a/glance/policies/image.py b/glance/policies/image.py new file mode 100644 index 0000000000..77ddd09ded --- /dev/null +++ b/glance/policies/image.py @@ -0,0 +1,47 @@ +# 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. + +from oslo_policy import policy + + +image_policies = [ + policy.RuleDefault(name="add_image", check_str="rule:default"), + policy.RuleDefault(name="delete_image", check_str="rule:default"), + policy.RuleDefault(name="get_image", check_str="rule:default"), + policy.RuleDefault(name="get_images", check_str="rule:default"), + policy.RuleDefault(name="modify_image", check_str="rule:default"), + policy.RuleDefault(name="publicize_image", check_str="role:admin"), + policy.RuleDefault(name="communitize_image", check_str="rule:default"), + policy.RuleDefault(name="copy_from", check_str="rule:default"), + + policy.RuleDefault(name="download_image", check_str="rule:default"), + policy.RuleDefault(name="upload_image", check_str="rule:default"), + + policy.RuleDefault(name="delete_image_location", check_str="rule:default"), + policy.RuleDefault(name="get_image_location", check_str="rule:default"), + policy.RuleDefault(name="set_image_location", check_str="rule:default"), + + policy.RuleDefault(name="add_member", check_str="rule:default"), + policy.RuleDefault(name="delete_member", check_str="rule:default"), + policy.RuleDefault(name="get_member", check_str="rule:default"), + policy.RuleDefault(name="get_members", check_str="rule:default"), + policy.RuleDefault(name="modify_member", check_str="rule:default"), + + policy.RuleDefault(name="manage_image_cache", check_str="role:admin"), + + policy.RuleDefault(name="deactivate", check_str="rule:default"), + policy.RuleDefault(name="reactivate", check_str="rule:default"), +] + + +def list_rules(): + return image_policies diff --git a/glance/policies/metadef.py b/glance/policies/metadef.py new file mode 100644 index 0000000000..55ddd6c827 --- /dev/null +++ b/glance/policies/metadef.py @@ -0,0 +1,52 @@ +# 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. + +from oslo_policy import policy + + +metadef_policies = [ + policy.RuleDefault(name="get_metadef_namespace", check_str="rule:default"), + policy.RuleDefault(name="get_metadef_namespaces", + check_str="rule:default"), + policy.RuleDefault(name="modify_metadef_namespace", + check_str="rule:default"), + policy.RuleDefault(name="add_metadef_namespace", check_str="rule:default"), + + policy.RuleDefault(name="get_metadef_object", check_str="rule:default"), + policy.RuleDefault(name="get_metadef_objects", check_str="rule:default"), + policy.RuleDefault(name="modify_metadef_object", check_str="rule:default"), + policy.RuleDefault(name="add_metadef_object", check_str="rule:default"), + + policy.RuleDefault(name="list_metadef_resource_types", + check_str="rule:default"), + policy.RuleDefault(name="get_metadef_resource_type", + check_str="rule:default"), + policy.RuleDefault(name="add_metadef_resource_type_association", + check_str="rule:default"), + + policy.RuleDefault(name="get_metadef_property", check_str="rule:default"), + policy.RuleDefault(name="get_metadef_properties", + check_str="rule:default"), + policy.RuleDefault(name="modify_metadef_property", + check_str="rule:default"), + policy.RuleDefault(name="add_metadef_property", check_str="rule:default"), + + policy.RuleDefault(name="get_metadef_tag", check_str="rule:default"), + policy.RuleDefault(name="get_metadef_tags", check_str="rule:default"), + policy.RuleDefault(name="modify_metadef_tag", check_str="rule:default"), + policy.RuleDefault(name="add_metadef_tag", check_str="rule:default"), + policy.RuleDefault(name="add_metadef_tags", check_str="rule:default"), +] + + +def list_rules(): + return metadef_policies diff --git a/glance/policies/tasks.py b/glance/policies/tasks.py new file mode 100644 index 0000000000..3dd674ed58 --- /dev/null +++ b/glance/policies/tasks.py @@ -0,0 +1,26 @@ +# 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. + +from oslo_policy import policy + + +task_policies = [ + policy.RuleDefault(name="get_task", check_str="rule:default"), + policy.RuleDefault(name="get_tasks", check_str="rule:default"), + policy.RuleDefault(name="add_task", check_str="rule:default"), + policy.RuleDefault(name="modify_task", check_str="rule:default"), + policy.RuleDefault(name="tasks_api_access", check_str="role:admin"), +] + + +def list_rules(): + return task_policies diff --git a/glance/tests/unit/test_policy.py b/glance/tests/unit/test_policy.py index 12ad8573e6..3dea40be59 100644 --- a/glance/tests/unit/test_policy.py +++ b/glance/tests/unit/test_policy.py @@ -162,6 +162,22 @@ class TaskFactoryStub(object): class TestPolicyEnforcer(base.IsolatedUnitTest): + def test_policy_enforce_unregistered(self): + enforcer = glance.api.policy.Enforcer() + context = glance.context.RequestContext(roles=[]) + + self.assertRaises(glance.api.policy.policy.PolicyNotRegistered, + enforcer.enforce, + context, 'wibble', {}) + + def test_policy_check_unregistered(self): + enforcer = glance.api.policy.Enforcer() + context = glance.context.RequestContext(roles=[]) + + self.assertRaises(glance.api.policy.policy.PolicyNotRegistered, + enforcer.check, + context, 'wibble', {}) + def test_policy_file_default_rules_default_location(self): enforcer = glance.api.policy.Enforcer() diff --git a/releasenotes/notes/policy-in-code-7e0c6c070d32d136.yaml b/releasenotes/notes/policy-in-code-7e0c6c070d32d136.yaml new file mode 100644 index 0000000000..033f67df10 --- /dev/null +++ b/releasenotes/notes/policy-in-code-7e0c6c070d32d136.yaml @@ -0,0 +1,22 @@ +--- +upgrade: + - | + Policy defaults are now defined in code, as they already were in other + OpenStack services. After upgrading there is no need to provide a + ``policy.json`` file (and you should not do so) unless you want to override + the default policies, and only policies you want to override need be + mentioned in the file. You should no longer rely on the ``default`` rule, + and especially not the default value of the rule (which has been relaxed), + to assign a non-default policy to rules not explicitly specified in the + policy file. +security: + - | + If the existing ``policy.json`` file relies on the ``default`` rule for + some policies (i.e. not all policies are explicitly specified in the file) + then the ``default`` rule must be explicitly set (e.g. to + ``"role:admin"``) in the file. The new default value for the ``default`` + rule is ``""``, whereas since the Queens release it has been + ``"role:admin"`` (prior to Queens it was ``"@"``, which allows everything). + After upgrading to this release, the policy file should be replaced by one + that overrides only policies that need to be different from the defaults, + without relying on the ``default`` rule. diff --git a/setup.cfg b/setup.cfg index 40479505ed..b833fdd0c0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -66,6 +66,8 @@ glance.database.metadata_backend = oslo.policy.enforcer = glance = glance.api.policy:get_enforcer +oslo.policy.policies = + glance = glance.policies:list_rules glance.flows = api_image_import = glance.async_.flows.api_image_import:get_flow diff --git a/tox.ini b/tox.ini index 6f720d7200..776b2f265b 100644 --- a/tox.ini +++ b/tox.ini @@ -69,6 +69,11 @@ whitelist_externals = commands = stestr run {posargs} +[testenv:genpolicy] +basepython = python3 +commands = + oslopolicy-sample-generator --config-file=etc/glance-policy-generator.conf + [testenv:gateonly] # NOTE(rosmaita): these tests catch configuration problems for some code # constants that must be maintained manually; we have them separated out