From 34b661ad081b3a89f422937cf722a63f231898ad Mon Sep 17 00:00:00 2001 From: Niraj Singh Date: Thu, 8 Aug 2019 08:59:06 +0000 Subject: [PATCH] Implement policy in code * Added policy in code as per community goals [1] for vnf packages. * Modified tox to generate policy sample json file. [1]: https://governance.openstack.org/tc/goals/queens/policy-in-code.html Partial-Implements: blueprint tosca-csar-mgmt-driver Co-Author: Neha Alhat Co-Author: Bhagyashri Shewale Change-Id: I7cedbca4abe41223e3f8d6211a74b4347299e9e5 --- .gitignore | 2 + devstack/lib/tacker | 6 -- doc/source/_extra/tacker.conf.sample | 1 - doc/source/conf.py | 7 ++ doc/source/configuration/index.rst | 21 +++++ doc/source/configuration/policy.rst | 9 +++ doc/source/configuration/sample_policy.rst | 16 ++++ etc/tacker-policy-generator.conf | 3 + etc/tacker/README-policy-yaml.txt | 7 ++ etc/tacker/policy.json | 10 --- setup.cfg | 10 ++- tacker/common/config.py | 2 - tacker/context.py | 29 +++++++ tacker/policies/__init__.py | 27 +++++++ tacker/policies/base.py | 49 ++++++++++++ tacker/policies/vnf_package.py | 91 ++++++++++++++++++++++ tacker/policy.py | 42 ++++++---- tacker/tests/unit/common/test_config.py | 1 - tox.ini | 5 ++ 19 files changed, 303 insertions(+), 35 deletions(-) create mode 100644 doc/source/configuration/policy.rst create mode 100644 doc/source/configuration/sample_policy.rst create mode 100644 etc/tacker-policy-generator.conf create mode 100644 etc/tacker/README-policy-yaml.txt delete mode 100644 etc/tacker/policy.json create mode 100644 tacker/policies/__init__.py create mode 100644 tacker/policies/base.py create mode 100644 tacker/policies/vnf_package.py diff --git a/.gitignore b/.gitignore index 77f6e2101..ef0087f19 100644 --- a/.gitignore +++ b/.gitignore @@ -28,7 +28,9 @@ subunit.log .eggs/ .stestr/ SP1_res.yaml +etc/tacker/policy.yaml.sample releasenotes/build etc/tacker/tacker.conf.sample doc/source/contributor/api +doc/source/_static/tacker.policy.yaml.sample diff --git a/devstack/lib/tacker b/devstack/lib/tacker index 77fec7618..f8978856d 100644 --- a/devstack/lib/tacker +++ b/devstack/lib/tacker @@ -200,16 +200,10 @@ function configure_tacker { # server TACKER_API_PASTE_FILE=$TACKER_CONF_DIR/api-paste.ini - TACKER_POLICY_FILE=$TACKER_CONF_DIR/policy.json cp $TACKER_DIR/etc/tacker/api-paste.ini $TACKER_API_PASTE_FILE - cp $TACKER_DIR/etc/tacker/policy.json $TACKER_POLICY_FILE - - # allow tacker user to administer tacker to match tacker account - sed -i 's/"context_is_admin": "role:admin"/"context_is_admin": "role:admin or user_name:tacker"/g' $TACKER_POLICY_FILE iniset $TACKER_CONF DEFAULT debug $ENABLE_DEBUG_LOG_LEVEL - iniset $TACKER_CONF DEFAULT policy_file $TACKER_POLICY_FILE iniset $TACKER_CONF DEFAULT auth_strategy $TACKER_AUTH_STRATEGY _tacker_setup_keystone $TACKER_CONF keystone_authtoken diff --git a/doc/source/_extra/tacker.conf.sample b/doc/source/_extra/tacker.conf.sample index fd61763f1..77326a704 100644 --- a/doc/source/_extra/tacker.conf.sample +++ b/doc/source/_extra/tacker.conf.sample @@ -1,6 +1,5 @@ [DEFAULT] auth_strategy = keystone -policy_file = /etc/tacker/policy.json debug = True logging_exception_prefix = %(color)s%(asctime)s.%(msecs)03d TRACE %(name)s %(instance)s logging_debug_format_suffix = from (pid=%(process)d) %(funcName)s %(pathname)s:%(lineno)d diff --git a/doc/source/conf.py b/doc/source/conf.py index 2c2b04154..ca9e2f8a8 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -23,8 +23,15 @@ extensions = [ 'sphinxcontrib.apidoc', 'stevedore.sphinxext', 'openstackdocstheme', + 'oslo_policy.sphinxext', + 'oslo_policy.sphinxpolicygen', ] +policy_generator_config_file = [ + ('../../etc/tacker-policy-generator.conf', '_static/tacker'), +] +sample_policy_basename = '_static/tacker' + # sphinxcontrib.apidoc options apidoc_module_dir = '../../tacker' apidoc_output_dir = 'contributor/api' diff --git a/doc/source/configuration/index.rst b/doc/source/configuration/index.rst index cf3d43885..d9c3752d4 100644 --- a/doc/source/configuration/index.rst +++ b/doc/source/configuration/index.rst @@ -31,3 +31,24 @@ The sample configuration can also be viewed in :download:`file form version of this documentation. .. literalinclude:: /_extra/tacker.conf.sample + +Policy +------ + +Tacker, like most OpenStack projects, uses a policy language to restrict +permissions on REST API actions. + +* :doc:`Policy Reference `: A complete reference of all + policy points in tacker and what they impact. + +* :doc:`Sample Policy File `: A sample tacker + policy file with inline documentation. + +.. # NOTE(bhagyashris): This is the section where we hide things that we don't + # actually want in the table of contents but sphinx build would fail if + # they aren't in the toctree somewhere. +.. toctree:: + :hidden: + + policy + sample_policy diff --git a/doc/source/configuration/policy.rst b/doc/source/configuration/policy.rst new file mode 100644 index 000000000..5e26f96e5 --- /dev/null +++ b/doc/source/configuration/policy.rst @@ -0,0 +1,9 @@ +=============== +Tacker Policies +=============== + +The following is an overview of all available policies in Tacker. +For a sample configuration file, refer to :doc:`/configuration/sample_policy`. + +.. show-policy:: + :config-file: etc/tacker-policy-generator.conf diff --git a/doc/source/configuration/sample_policy.rst b/doc/source/configuration/sample_policy.rst new file mode 100644 index 000000000..0ab9969e1 --- /dev/null +++ b/doc/source/configuration/sample_policy.rst @@ -0,0 +1,16 @@ +========================= +Sample Tacker Policy File +========================= + +The following is a sample tacker policy file for adaptation and use. + +The sample policy can also be viewed in :download:`file form +`. + +.. important:: + + The sample policy file is auto-generated from tacker when this documentation + is built. You must ensure your version of tacker matches the version of this + documentation. + +.. literalinclude:: /_static/tacker.policy.yaml.sample diff --git a/etc/tacker-policy-generator.conf b/etc/tacker-policy-generator.conf new file mode 100644 index 000000000..f8ab0344c --- /dev/null +++ b/etc/tacker-policy-generator.conf @@ -0,0 +1,3 @@ +[DEFAULT] +output_file = etc/tacker/policy.yaml.sample +namespace = tacker diff --git a/etc/tacker/README-policy-yaml.txt b/etc/tacker/README-policy-yaml.txt new file mode 100644 index 000000000..22aab576f --- /dev/null +++ b/etc/tacker/README-policy-yaml.txt @@ -0,0 +1,7 @@ +Tacker +====== + +To generate the sample tacker policy.yaml file, run the following command from +the top level of the tacker directory: + + tox -egenpolicy diff --git a/etc/tacker/policy.json b/etc/tacker/policy.json deleted file mode 100644 index b38bc692c..000000000 --- a/etc/tacker/policy.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "context_is_admin": "role:admin", - "admin_or_owner": "rule:context_is_admin or tenant_id:%(tenant_id)s", - "admin_only": "rule:context_is_admin", - "regular_user": "", - "shared": "field:vims:shared=True", - "default": "rule:admin_or_owner", - - "get_vim": "rule:admin_or_owner or rule:shared" -} diff --git a/setup.cfg b/setup.cfg index 620a59496..a30dca7c1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -25,7 +25,6 @@ packages = data_files = etc/tacker = etc/tacker/api-paste.ini - etc/tacker/policy.json etc/tacker/rootwrap.conf etc/rootwrap.d = etc/tacker/rootwrap.d/tacker.filters @@ -96,6 +95,15 @@ oslo.config.opts = mistral.actions = tacker.vim_ping_action = tacker.nfvo.workflows.vim_monitor.vim_ping_action:PingVimAction +oslo.policy.enforcer = + tacker = tacker.policy:get_enforcer + +oslo.policy.policies = + # The sample policies will be ordered by entry point and then by list + # returned from that entry point. If more control is desired split out each + # list_rules method into a separate entry point rather than using the + # aggregate method. + tacker = tacker.policies:list_rules [build_releasenotes] all_files = 1 diff --git a/tacker/common/config.py b/tacker/common/config.py index cb1bf06a8..2d5176c4e 100644 --- a/tacker/common/config.py +++ b/tacker/common/config.py @@ -43,8 +43,6 @@ core_opts = [ help=_("The path for API extensions")), cfg.ListOpt('service_plugins', default=['nfvo', 'vnfm'], help=_("The service plugins Tacker will use")), - cfg.StrOpt('policy_file', default="policy.json", - help=_("The policy file to use")), cfg.StrOpt('auth_strategy', default='keystone', help=_("The type of authentication to use")), cfg.BoolOpt('allow_bulk', default=True, diff --git a/tacker/context.py b/tacker/context.py index 9d99d1695..e6078b736 100644 --- a/tacker/context.py +++ b/tacker/context.py @@ -23,6 +23,7 @@ from oslo_config import cfg from oslo_context import context as oslo_context from oslo_db.sqlalchemy import enginefacade +from tacker.common import exceptions from tacker.db import api as db_api from tacker import policy @@ -142,6 +143,34 @@ class ContextBase(oslo_context.RequestContext): return context + def can(self, action, target=None, fatal=True): + """Verifies that the given action is valid on the target in this context. + + :param action: string representing the action to be checked. + :param target: dictionary representing the object of the action + for object creation this should be a dictionary representing the + location of the object e.g. ``{'project_id': context.project_id}``. + If None, then this default target will be considered: + {'project_id': self.project_id, 'user_id': self.user_id} + :param fatal: if False, will return False when an exception.Forbidden + occurs. + + :raises tacker.exception.Forbidden: if verification fails and fatal + is True. + + :return: returns a non-False value (not necessarily "True") if + authorized and False if not authorized and fatal is False. + """ + if target is None: + target = {'tenant_id': self.tenant_id, + 'user_id': self.user_id} + try: + return policy.authorize(self, action, target) + except exceptions.Forbidden: + if fatal: + raise + return False + @enginefacade.transaction_context_provider class ContextBaseWithSession(ContextBase): diff --git a/tacker/policies/__init__.py b/tacker/policies/__init__.py new file mode 100644 index 000000000..b0a945869 --- /dev/null +++ b/tacker/policies/__init__.py @@ -0,0 +1,27 @@ +# Copyright (C) 2019 NTT DATA +# All Rights Reserved. +# +# 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 tacker.policies import base +from tacker.policies import vnf_package + + +def list_rules(): + return itertools.chain( + base.list_rules(), + vnf_package.list_rules(), + ) diff --git a/tacker/policies/base.py b/tacker/policies/base.py new file mode 100644 index 000000000..e7812a016 --- /dev/null +++ b/tacker/policies/base.py @@ -0,0 +1,49 @@ +# Copyright (C) 2019 NTT DATA +# All Rights Reserved. +# +# 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 + +TACKER_API = 'os_nfv_orchestration_api' + +RULE_ADMIN_OR_OWNER = 'rule:admin_or_owner' +RULE_ADMIN_API = 'rule:admin_only' +RULE_ANY = '@' + +rules = [ + policy.RuleDefault( + "context_is_admin", + "role:admin", + "Decides what is required for the 'is_admin:True' check to succeed."), + policy.RuleDefault( + "admin_or_owner", + "is_admin:True or tenant_id:%(tenant_id)s", + "Default rule for most non-Admin APIs."), + policy.RuleDefault( + "admin_only", + "is_admin:True", + "Default rule for most Admin APIs."), + policy.RuleDefault( + "shared", + "field:vims:shared=True", + "Default rule for sharing vims."), + policy.RuleDefault( + "default", + "rule:admin_or_owner", + "Default rule for most non-Admin APIs.") +] + + +def list_rules(): + return rules diff --git a/tacker/policies/vnf_package.py b/tacker/policies/vnf_package.py new file mode 100644 index 000000000..695b9d438 --- /dev/null +++ b/tacker/policies/vnf_package.py @@ -0,0 +1,91 @@ +# Copyright (C) 2019 NTT DATA +# All Rights Reserved. +# +# 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 tacker.policies import base + + +VNFPKGM = 'os_nfv_orchestration_api:vnf_packages:%s' + +rules = [ + policy.DocumentedRuleDefault( + name=VNFPKGM % 'create', + check_str=base.RULE_ADMIN_OR_OWNER, + description="Creates a vnf package.", + operations=[ + { + 'method': 'POST', + 'path': '/vnf_packages' + } + ]), + policy.DocumentedRuleDefault( + name=VNFPKGM % 'show', + check_str=base.RULE_ADMIN_OR_OWNER, + description="Show a vnf package.", + operations=[ + { + 'method': 'GET', + 'path': '/vnf_packages/{vnf_package_id}' + } + ]), + policy.DocumentedRuleDefault( + name=VNFPKGM % 'index', + check_str=base.RULE_ADMIN_OR_OWNER, + description="List all vnf packages.", + operations=[ + { + 'method': 'GET', + 'path': '/vnf_packages/' + } + ]), + policy.DocumentedRuleDefault( + name=VNFPKGM % 'delete', + check_str=base.RULE_ADMIN_OR_OWNER, + description="Delete a vnf package.", + operations=[ + { + 'method': 'DELETE', + 'path': '/vnf_packages/{vnf_package_id}' + } + ]), + policy.DocumentedRuleDefault( + name=VNFPKGM % 'upload_package_content', + check_str=base.RULE_ADMIN_OR_OWNER, + description="upload a vnf package content.", + operations=[ + { + 'method': 'PUT', + 'path': '/vnf_packages/{vnf_package_id}/' + 'package_content' + } + ]), + policy.DocumentedRuleDefault( + name=VNFPKGM % 'upload_from_uri', + check_str=base.RULE_ADMIN_OR_OWNER, + description="upload a vnf package content from uri.", + operations=[ + { + 'method': 'POST', + 'path': '/vnf_packages/{vnf_package_id}/package_content/' + 'upload_from_uri' + } + ]), +] + + +def list_rules(): + return rules diff --git a/tacker/policy.py b/tacker/policy.py index 82ee9e43c..c5cfb4305 100644 --- a/tacker/policy.py +++ b/tacker/policy.py @@ -28,6 +28,7 @@ import six from tacker._i18n import _ from tacker.api.v1 import attributes from tacker.common import exceptions +from tacker import policies LOG = logging.getLogger(__name__) @@ -36,18 +37,6 @@ _ENFORCER = None ADMIN_CTX_POLICY = 'context_is_admin' -_BASE_RULES = [ - policy.RuleDefault( - ADMIN_CTX_POLICY, - 'role:admin', - description='Rule for cloud admin access'), - # policy.RuleDefault( - # _ADVSVC_CTX_POLICY, - # 'role:advsvc', - # description='Rule for advanced service role access'), -] - - def reset(): global _ENFORCER if _ENFORCER: @@ -61,8 +50,29 @@ def init(conf=cfg.CONF, policy_file=None): global _ENFORCER if not _ENFORCER: _ENFORCER = policy.Enforcer(conf, policy_file=policy_file) - _ENFORCER.register_defaults(_BASE_RULES) - _ENFORCER.load_rules(True) + register_rules(_ENFORCER) + _ENFORCER.load_rules() + + +def authorize(context, action, target, do_raise=True, exc=None): + + init() + credentials = context.to_policy_values() + if not exc: + exc = exceptions.PolicyNotAuthorized + try: + result = _ENFORCER.authorize(action, target, credentials, + do_raise=do_raise, exc=exc, action=action) + except policy.PolicyNotRegistered: + with excutils.save_and_reraise_exception(): + LOG.debug('Policy not registered') + except Exception: + with excutils.save_and_reraise_exception(): + LOG.debug('Policy check for %(action)s failed with credentials ' + '%(credentials)s', + {'action': action, 'credentials': credentials}) + + return result def refresh(policy_file=None): @@ -427,6 +437,10 @@ def check_is_admin(context): return False +def register_rules(enforcer): + enforcer.register_defaults(policies.list_rules()) + + def get_enforcer(): # NOTE(amotoki): This was borrowed from nova/policy.py. # This method is for use by oslo.policy CLI scripts. Those scripts need the diff --git a/tacker/tests/unit/common/test_config.py b/tacker/tests/unit/common/test_config.py index 3041f84e8..8d6eb2f74 100644 --- a/tacker/tests/unit/common/test_config.py +++ b/tacker/tests/unit/common/test_config.py @@ -29,7 +29,6 @@ class ConfigurationTest(base.BaseTestCase): self.assertEqual(9890, cfg.CONF.bind_port) self.assertEqual('api-paste.ini.test', cfg.CONF.api_paste_config) self.assertEqual('unit/extensions', cfg.CONF.api_extensions_path) - self.assertEqual('policy.json', cfg.CONF.policy_file) self.assertEqual('keystone', cfg.CONF.auth_strategy) self.assertTrue(cfg.CONF.allow_bulk) self.assertFalse(cfg.CONF.allow_pagination) diff --git a/tox.ini b/tox.ini index be5a28c59..cd920f8bd 100644 --- a/tox.ini +++ b/tox.ini @@ -60,6 +60,7 @@ commands = python ./tools/check_i18n.py ./tacker deps = -r{toxinidir}/doc/requirements.txt commands = sphinx-build -W -b html doc/source doc/build/html + oslopolicy-sample-generator --config-file=etc/tacker-policy-generator.conf [testenv:api-ref] deps = -r{toxinidir}/doc/requirements.txt @@ -101,6 +102,10 @@ local-check-factory = tacker.hacking.checks.factory commands = oslo-config-generator --config-file=etc/config-generator.conf +[testenv:genpolicy] +commands = + oslopolicy-sample-generator --config-file=etc/tacker-policy-generator.conf + [testenv:lower-constraints] deps = -c{toxinidir}/lower-constraints.txt