From 99cb6b5199cd61cea218bd11848b11e2b1baaff4 Mon Sep 17 00:00:00 2001 From: Hiroaki Kobayashi Date: Tue, 5 Jun 2018 11:35:06 +0900 Subject: [PATCH] Register and document policy in code This patch moves default policy definitions from file-based maintenance to registering them in code following OpenStack standard[1]. [1] https://governance.openstack.org/tc/goals/queens/policy-in-code.html Change-Id: I47afa929ebfa30e17c2dbeac31108ecbab67f067 Implements: blueprint policy-in-code --- .gitignore | 3 + blazar/api/v1/oshosts/service.py | 4 +- blazar/api/v1/service.py | 4 +- blazar/api/v2/controllers/extensions/host.py | 4 +- blazar/api/v2/controllers/extensions/lease.py | 4 +- blazar/policies/__init__.py | 25 +++++++ blazar/policies/base.py | 32 +++++++++ blazar/policies/leases.py | 72 +++++++++++++++++++ blazar/policies/oshosts.py | 72 +++++++++++++++++++ blazar/policy.py | 2 + blazar/tests/__init__.py | 21 ------ blazar/tests/fake_policy.py | 39 ---------- blazar/tests/test_policy.py | 23 ++---- devstack/plugin.sh | 3 - doc/source/conf.py | 9 ++- doc/source/configuration/blazar-policy.rst | 9 ++- .../configuration/samples/blazar-policy.rst | 13 +++- .../install/install-without-devstack.rst | 8 --- etc/blazar/blazar-policy-generator.conf | 3 + etc/policy.json | 20 ------ setup.cfg | 3 + tox.ini | 5 ++ 22 files changed, 259 insertions(+), 119 deletions(-) create mode 100644 blazar/policies/__init__.py create mode 100644 blazar/policies/base.py create mode 100644 blazar/policies/leases.py create mode 100644 blazar/policies/oshosts.py delete mode 100644 blazar/tests/fake_policy.py create mode 100644 etc/blazar/blazar-policy-generator.conf delete mode 100644 etc/policy.json diff --git a/.gitignore b/.gitignore index fd1151f3..f87f63cd 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,6 @@ AUTHORS ChangeLog .testrepository + +# generated policy file +etc/blazar/policy.yaml.sample \ No newline at end of file diff --git a/blazar/api/v1/oshosts/service.py b/blazar/api/v1/oshosts/service.py index 4823377c..fdf88668 100644 --- a/blazar/api/v1/oshosts/service.py +++ b/blazar/api/v1/oshosts/service.py @@ -27,7 +27,7 @@ class API(object): """List all existing computehosts.""" return self.manager_rpcapi.list_computehosts() - @policy.authorize('oshosts', 'create') + @policy.authorize('oshosts', 'post') @trusts.use_trust_auth() def create_computehost(self, data): """Create new computehost. @@ -47,7 +47,7 @@ class API(object): """ return self.manager_rpcapi.get_computehost(host_id) - @policy.authorize('oshosts', 'update') + @policy.authorize('oshosts', 'put') def update_computehost(self, host_id, data): """Update computehost. Only name changing may be proceeded. diff --git a/blazar/api/v1/service.py b/blazar/api/v1/service.py index 5d262fdc..704666e3 100644 --- a/blazar/api/v1/service.py +++ b/blazar/api/v1/service.py @@ -40,7 +40,7 @@ class API(object): project_id = ctx.project_id return self.manager_rpcapi.list_leases(project_id=project_id) - @policy.authorize('leases', 'create') + @policy.authorize('leases', 'post') @trusts.use_trust_auth() def create_lease(self, data): """Create new lease. @@ -64,7 +64,7 @@ class API(object): """ return self.manager_rpcapi.get_lease(lease_id) - @policy.authorize('leases', 'update') + @policy.authorize('leases', 'put') def update_lease(self, lease_id, data): """Update lease. diff --git a/blazar/api/v2/controllers/extensions/host.py b/blazar/api/v2/controllers/extensions/host.py index f61cbf20..4053b46e 100644 --- a/blazar/api/v2/controllers/extensions/host.py +++ b/blazar/api/v2/controllers/extensions/host.py @@ -126,7 +126,7 @@ class HostsController(extensions.BaseController): for host in pecan.request.hosts_rpcapi.list_computehosts()] - @policy.authorize('oshosts', 'create') + @policy.authorize('oshosts', 'post') @wsme_pecan.wsexpose(Host, body=Host, status_code=202) @trusts.use_trust_auth() def post(self, host): @@ -144,7 +144,7 @@ class HostsController(extensions.BaseController): else: raise exceptions.BlazarException(_("Host can't be created")) - @policy.authorize('oshosts', 'update') + @policy.authorize('oshosts', 'put') @wsme_pecan.wsexpose(Host, types.IntegerType(), body=Host, status_code=202) def put(self, id, host): diff --git a/blazar/api/v2/controllers/extensions/lease.py b/blazar/api/v2/controllers/extensions/lease.py index 11279f8a..0cfb3576 100644 --- a/blazar/api/v2/controllers/extensions/lease.py +++ b/blazar/api/v2/controllers/extensions/lease.py @@ -103,7 +103,7 @@ class LeasesController(extensions.BaseController): return [Lease.convert(lease) for lease in pecan.request.rpcapi.list_leases()] - @policy.authorize('leases', 'create') + @policy.authorize('leases', 'post') @wsme_pecan.wsexpose(Lease, body=Lease, status_code=202) @trusts.use_trust_auth() def post(self, lease): @@ -120,7 +120,7 @@ class LeasesController(extensions.BaseController): else: raise exceptions.BlazarException(_("Lease can't be created")) - @policy.authorize('leases', 'update') + @policy.authorize('leases', 'put') @wsme_pecan.wsexpose(Lease, types.UuidType(), body=Lease, status_code=202) def put(self, id, sublease): """Update an existing lease. diff --git a/blazar/policies/__init__.py b/blazar/policies/__init__.py new file mode 100644 index 00000000..2fde2952 --- /dev/null +++ b/blazar/policies/__init__.py @@ -0,0 +1,25 @@ +# 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 blazar.policies import base +from blazar.policies import leases +from blazar.policies import oshosts + + +def list_rules(): + return itertools.chain( + base.list_rules(), + leases.list_rules(), + oshosts.list_rules(), + ) diff --git a/blazar/policies/base.py b/blazar/policies/base.py new file mode 100644 index 00000000..2bd7f64a --- /dev/null +++ b/blazar/policies/base.py @@ -0,0 +1,32 @@ +# 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 + +RULE_ADMIN = 'rule:admin' +RULE_ADMIN_OR_OWNER = 'rule:admin_or_owner' +RULE_ANY = '@' + +rules = [ + policy.RuleDefault( + name="admin", + check_str="is_admin:True or role:admin", + description="Default rule for most Admin APIs."), + policy.RuleDefault( + name="admin_or_owner", + check_str="rule:admin or project_id:%(project_id)s", + description="Default rule for most non-Admin APIs.") +] + + +def list_rules(): + return rules diff --git a/blazar/policies/leases.py b/blazar/policies/leases.py new file mode 100644 index 00000000..e209f69a --- /dev/null +++ b/blazar/policies/leases.py @@ -0,0 +1,72 @@ +# 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 + +from blazar.policies import base + +POLICY_ROOT = 'blazar:leases:%s' + +leases_policies = [ + policy.DocumentedRuleDefault( + name=POLICY_ROOT % 'get', + check_str=base.RULE_ADMIN_OR_OWNER, + description='Policy rule for List/Show Lease(s) API.', + operations=[ + { + 'path': '/{api_version}/leases', + 'method': 'GET' + }, + { + 'path': '/{api_version}/leases/{lease_id}', + 'method': 'GET' + } + ] + ), + policy.DocumentedRuleDefault( + name=POLICY_ROOT % 'post', + check_str=base.RULE_ADMIN_OR_OWNER, + description='Policy rule for Create Lease API.', + operations=[ + { + 'path': '/{api_version}/leases', + 'method': 'POST' + } + ] + ), + policy.DocumentedRuleDefault( + name=POLICY_ROOT % 'put', + check_str=base.RULE_ADMIN_OR_OWNER, + description='Policy rule for Update Lease API.', + operations=[ + { + 'path': '/{api_version}/leases/{lease_id}', + 'method': 'PUT' + } + ] + ), + policy.DocumentedRuleDefault( + name=POLICY_ROOT % 'delete', + check_str=base.RULE_ADMIN_OR_OWNER, + description='Policy rule for Delete Lease API.', + operations=[ + { + 'path': '/{api_version}/leases/{lease_id}', + 'method': 'DELETE' + } + ] + ) +] + + +def list_rules(): + return leases_policies diff --git a/blazar/policies/oshosts.py b/blazar/policies/oshosts.py new file mode 100644 index 00000000..04c4873c --- /dev/null +++ b/blazar/policies/oshosts.py @@ -0,0 +1,72 @@ +# 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 + +from blazar.policies import base + +POLICY_ROOT = 'blazar:oshosts:%s' + +oshosts_policies = [ + policy.DocumentedRuleDefault( + name=POLICY_ROOT % 'get', + check_str=base.RULE_ADMIN, + description='Policy rule for List/Show Host(s) API.', + operations=[ + { + 'path': '/{api_version}/os-hosts', + 'method': 'GET' + }, + { + 'path': '/{api_version}/os-hosts/{host_id}', + 'method': 'GET' + } + ] + ), + policy.DocumentedRuleDefault( + name=POLICY_ROOT % 'post', + check_str=base.RULE_ADMIN, + description='Policy rule for Create Host API.', + operations=[ + { + 'path': '/{api_version}/os-hosts', + 'method': 'POST' + } + ] + ), + policy.DocumentedRuleDefault( + name=POLICY_ROOT % 'put', + check_str=base.RULE_ADMIN, + description='Policy rule for Update Host API.', + operations=[ + { + 'path': '/{api_version}/os-hosts/{host_id}', + 'method': 'PUT' + } + ] + ), + policy.DocumentedRuleDefault( + name=POLICY_ROOT % 'delete', + check_str=base.RULE_ADMIN, + description='Policy rule for Delete Host API.', + operations=[ + { + 'path': '/{api_version}/os-hosts/{host_id}', + 'method': 'DELETE' + } + ] + ) +] + + +def list_rules(): + return oshosts_policies diff --git a/blazar/policy.py b/blazar/policy.py index d5acfcb9..37b7f9e3 100644 --- a/blazar/policy.py +++ b/blazar/policy.py @@ -24,6 +24,7 @@ from oslo_policy import policy from blazar import context from blazar import exceptions +from blazar import policies CONF = cfg.CONF opts.set_defaults(CONF) @@ -45,6 +46,7 @@ def init(): if not _ENFORCER: LOG.debug("Enforcer not present, recreating at init stage.") _ENFORCER = policy.Enforcer(CONF) + _ENFORCER.register_defaults(policies.list_rules()) def set_rules(data, default_rule=None): diff --git a/blazar/tests/__init__.py b/blazar/tests/__init__.py index 04c3d226..ad2e15e1 100644 --- a/blazar/tests/__init__.py +++ b/blazar/tests/__init__.py @@ -13,8 +13,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import os - import fixtures import tempfile import testscenarios @@ -26,8 +24,6 @@ from oslotest import base from blazar import context from blazar.db.sqlalchemy import api as db_api from blazar.db.sqlalchemy import facade_wrapper -from blazar import policy -from blazar.tests import fake_policy cfg.CONF.set_override('use_stderr', False) @@ -52,22 +48,6 @@ class Database(fixtures.Fixture): self.addCleanup(db_api.drop_db) -class PolicyFixture(fixtures.Fixture): - - def setUp(self): - super(PolicyFixture, self).setUp() - self.policy_dir = self.useFixture(fixtures.TempDir()) - self.policy_file_name = os.path.join(self.policy_dir.path, - 'policy.json') - with open(self.policy_file_name, 'w') as policy_file: - policy_file.write(fake_policy.policy_data) - cfg.CONF.set_override('policy_file', self.policy_file_name, - group='oslo_policy') - policy.reset() - policy.init() - self.addCleanup(policy.reset) - - class TestCase(testscenarios.WithScenarios, base.BaseTestCase): """Test case base class for all unit tests. @@ -80,7 +60,6 @@ class TestCase(testscenarios.WithScenarios, base.BaseTestCase): super(TestCase, self).setUp() self.context_mock = None cfg.CONF(args=[], project='blazar') - self.policy = self.useFixture(PolicyFixture()) def patch(self, obj, attr): """Returns a Mocked object on the patched attribute.""" diff --git a/blazar/tests/fake_policy.py b/blazar/tests/fake_policy.py deleted file mode 100644 index b7b87386..00000000 --- a/blazar/tests/fake_policy.py +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright (c) 2013 Bull. -# -# 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. - - -policy_data = """ -{ - - "admin": "is_admin:True or role:admin or role:masterofuniverse", - "admin_or_owner": "rule:admin or project_id:%(project_id)s", - "default": "!", - - "admin_api": "rule:admin", - "blazar:leases": "rule:admin_or_owner", - "blazar:oshosts": "rule:admin_api", - - "blazar:leases:get": "rule:admin_or_owner", - "blazar:leases:create": "rule:admin_or_owner", - "blazar:leases:delete": "rule:admin_or_owner", - "blazar:leases:update": "rule:admin_or_owner", - - "blazar:plugins:get": "@", - - "blazar:oshosts:get": "rule:admin_api", - "blazar:oshosts:create": "rule:admin_api", - "blazar:oshosts:delete": "rule:admin_api", - "blazar:oshosts:update": "rule:admin_api" -} -""" diff --git a/blazar/tests/test_policy.py b/blazar/tests/test_policy.py index d37f694a..f7da9356 100644 --- a/blazar/tests/test_policy.py +++ b/blazar/tests/test_policy.py @@ -39,7 +39,7 @@ class BlazarPolicyTestCase(tests.TestCase): 'project_id': self.context.project_id} target_wrong = {'user_id': self.context.user_id, 'project_id': 'bad_project'} - action = "blazar:leases" + action = "blazar:leases:get" self.assertTrue(policy.enforce(self.context, action, target_good)) self.assertFalse(policy.enforce(self.context, action, @@ -48,14 +48,14 @@ class BlazarPolicyTestCase(tests.TestCase): def test_adminpolicy(self): target = {'user_id': self.context.user_id, 'project_id': self.context.project_id} - action = "blazar:oshosts" + action = "blazar:oshosts:get" self.assertRaises(exceptions.PolicyNotAuthorized, policy.enforce, self.context, action, target) def test_elevatedpolicy(self): target = {'user_id': self.context.user_id, 'project_id': self.context.project_id} - action = "blazar:oshosts" + action = "blazar:oshosts:get" self.assertRaises(exceptions.PolicyNotAuthorized, policy.enforce, self.context, action, target) elevated_context = self.context.elevated() @@ -63,23 +63,14 @@ class BlazarPolicyTestCase(tests.TestCase): def test_authorize(self): - @policy.authorize('leases', ctx=self.context) - def user_method(self): - return True - @policy.authorize('leases', 'get', ctx=self.context) def user_method_with_action(self): return True - @policy.authorize('oshosts', ctx=self.context) - def adminonly_method(self): + @policy.authorize('oshosts', 'get', ctx=self.context) + def adminonly_method_with_action(self): return True - self.assertTrue(user_method(self)) self.assertTrue(user_method_with_action(self)) - try: - adminonly_method(self) - self.assertTrue(False) - except exceptions.PolicyNotAuthorized: - # We are expecting this exception - self.assertTrue(True) + self.assertRaises(exceptions.PolicyNotAuthorized, + adminonly_method_with_action, self) diff --git a/devstack/plugin.sh b/devstack/plugin.sh index e1fc7e2b..8b154885 100644 --- a/devstack/plugin.sh +++ b/devstack/plugin.sh @@ -41,9 +41,6 @@ function configure_blazar { fi sudo chown $STACK_USER $BLAZAR_CONF_DIR - BLAZAR_POLICY_FILE=$BLAZAR_CONF_DIR/policy.json - cp $BLAZAR_DIR/etc/policy.json $BLAZAR_POLICY_FILE - touch $BLAZAR_CONF_FILE iniset $BLAZAR_CONF_FILE DEFAULT os_auth_version v3 diff --git a/doc/source/conf.py b/doc/source/conf.py index 0d7c7679..fe7396c0 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -39,6 +39,8 @@ extensions = ['sphinx.ext.autodoc', 'openstackdocstheme', 'oslo_config.sphinxext', 'oslo_config.sphinxconfiggen', + 'oslo_policy.sphinxpolicygen', + 'oslo_policy.sphinxext', ] # openstackdocstheme options @@ -46,11 +48,16 @@ repository_name = 'openstack/blazar' bug_project = 'blazar' bug_tag = '' +wsme_protocols = ['restjson', 'restxml'] + # oslo_config.sphinxconfiggen options config_generator_config_file = '../../etc/blazar/blazar-config-generator.conf' sample_config_basename = '_static/blazar' -wsme_protocols = ['restjson', 'restxml'] +# oslo_policy.sphinxpolicygen options +policy_generator_config_file = [ + ('../../etc/blazar/blazar-policy-generator.conf', '_static/blazar'), +] # The suffix of source filenames. source_suffix = '.rst' diff --git a/doc/source/configuration/blazar-policy.rst b/doc/source/configuration/blazar-policy.rst index 3bd4dfcf..c8415d9c 100644 --- a/doc/source/configuration/blazar-policy.rst +++ b/doc/source/configuration/blazar-policy.rst @@ -2,4 +2,11 @@ Policies ======== -Please see a sample policy file: :doc:`/configuration/samples/blazar-policy`. +The following is an overview of all available policies in Blazar. For a sample +configuration file, refer to :doc:`/configuration/samples/blazar-policy`. + +To change policies, please create a policy file in */etc/blazar/* and specify +the policy file name at the *oslo_policy/policy_file* option in *blazar.conf*. + +.. show-policy:: + :config-file: etc/blazar/blazar-policy-generator.conf \ No newline at end of file diff --git a/doc/source/configuration/samples/blazar-policy.rst b/doc/source/configuration/samples/blazar-policy.rst index e2be42ee..85a29215 100644 --- a/doc/source/configuration/samples/blazar-policy.rst +++ b/doc/source/configuration/samples/blazar-policy.rst @@ -2,6 +2,15 @@ Sample Policy File ================== -The following is a sample policy file. +The following is a sample blazar policy file for adaptation and use. -.. literalinclude:: ../../../../etc/policy.json +The sample policy can also be viewed in :download:`file form +`. + +.. important:: + + The sample policy file is auto-generated from blazar when this + documentation is built. You must ensure your version of blazar matches + the version of this documentation. + +.. literalinclude:: /_static/blazar.policy.yaml.sample \ No newline at end of file diff --git a/doc/source/install/install-without-devstack.rst b/doc/source/install/install-without-devstack.rst index e009f1db..de02f071 100644 --- a/doc/source/install/install-without-devstack.rst +++ b/doc/source/install/install-without-devstack.rst @@ -32,14 +32,6 @@ or .. -Next you need to create a Blazar policy file: - -.. sourcecode:: console - - cp /path/to/blazar/etc/policy.json /etc/blazar/ - -.. - Next you need to configure Blazar and Nova. First, generate a blazar.conf sample: .. sourcecode:: console diff --git a/etc/blazar/blazar-policy-generator.conf b/etc/blazar/blazar-policy-generator.conf new file mode 100644 index 00000000..4ced6dfa --- /dev/null +++ b/etc/blazar/blazar-policy-generator.conf @@ -0,0 +1,3 @@ +[DEFAULT] +output_file = etc/blazar/policy.yaml.sample +namespace = blazar diff --git a/etc/policy.json b/etc/policy.json deleted file mode 100644 index 251a6c8b..00000000 --- a/etc/policy.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "admin": "is_admin:True or role:admin or role:masterofuniverse", - - "admin_or_owner": "rule:admin or project_id:%(project_id)s", - - "default": "!", - "admin_api": "rule:admin", - - "blazar:leases:get": "rule:admin_or_owner", - "blazar:leases:create": "rule:admin_or_owner", - "blazar:leases:delete": "rule:admin_or_owner", - "blazar:leases:update": "rule:admin_or_owner", - - "blazar:plugins:get": "@", - - "blazar:oshosts:get": "rule:admin_or_owner", - "blazar:oshosts:create": "rule:admin_api", - "blazar:oshosts:delete": "rule:admin_api", - "blazar:oshosts:update": "rule:admin_api" -} diff --git a/setup.cfg b/setup.cfg index e915d51a..0625100f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -46,6 +46,9 @@ blazar.api.v2.controllers.extensions = oslo.config.opts = blazar = blazar.opts:list_opts +oslo.policy.policies = + blazar = blazar.policies:list_rules + wsgi_scripts = blazar-api-wsgi = blazar.api.wsgi_app:init_app diff --git a/tox.ini b/tox.ini index 5afc4cbd..50f530d6 100644 --- a/tox.ini +++ b/tox.ini @@ -60,6 +60,11 @@ commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasen commands = rm -rf api-ref/build sphinx-build -WE -b html -d api-ref/build/doctrees api-ref/source api-ref/build/html + +[testenv:genpolicy] +commands = + oslopolicy-sample-generator --config-file etc/blazar/blazar-policy-generator.conf + [testenv:lower-constraints] basepython = python3 deps =