From ecd29999cbb057c9252919176fdf5bba228adfab Mon Sep 17 00:00:00 2001 From: JinLi Date: Fri, 21 Jul 2017 15:28:13 -0700 Subject: [PATCH] Policies in yaml Implement functions allowing to define object_level policies in model's yaml file Change-Id: I4a4b70edf95c56d8dba7ee669d5ccc8bd387c4d8 --- doc/samples/policy.json | 6 + gluon/api/hooks/policy_enforcement.py | 57 +++++- gluon/managers/manager_base.py | 4 + gluon/models/base/base.yaml | 36 +++- gluon/models/net-l3vpn/api.yaml | 15 ++ gluon/particleGenerator/ApiGenerator.py | 8 + gluon/particleGenerator/PolicyGenerator.py | 44 +++++ gluon/particleGenerator/generator.py | 26 +++ gluon/policies/__init__.py | 8 +- gluon/policies/base.py | 11 ++ gluon/policies/net_l3vpn.py | 214 +++++++++++++++++++++ 11 files changed, 420 insertions(+), 9 deletions(-) create mode 100644 gluon/particleGenerator/PolicyGenerator.py create mode 100644 gluon/policies/net_l3vpn.py diff --git a/doc/samples/policy.json b/doc/samples/policy.json index 181b4ad..ff1f3bf 100644 --- a/doc/samples/policy.json +++ b/doc/samples/policy.json @@ -1,4 +1,9 @@ { + "COMMENT": "This file is no longer needed, but for historical record !!!", + "COMMENT": "The policy.json file in /etc/proton directory should contain", + "COMMENT": "empty json object: {}", + + "COMMENT": "This first part is moved to code in policies/base.py", "context_is_admin": "role:admin", "owner": "tenant_id:%(tenant_id)s", "admin_or_owner": "rule:context_is_admin or rule:owner", @@ -9,6 +14,7 @@ "regular_user": "", "default": "rule:admin_or_owner", + "COMMENT": "The rest of policies are defined in YAML", "create_ports": "rule:admin_or_network_owner", "get_ports": "rule:admin_or_owner", "update_ports": "rule:admin_or_network_owner", diff --git a/gluon/api/hooks/policy_enforcement.py b/gluon/api/hooks/policy_enforcement.py index 3e1a9e9..4fd4735 100644 --- a/gluon/api/hooks/policy_enforcement.py +++ b/gluon/api/hooks/policy_enforcement.py @@ -13,14 +13,17 @@ # License for the specific language governing permissions and limitations # under the License. +import json import webob from oslo_config import cfg from oslo_policy import policy as oslo_policy # from oslo_utils import excutils from pecan import hooks +from pecan.routing import lookup_controller from gluon import constants as gluon_constants +from gluon.particleGenerator.ApiGenerator import API_OBJECT_CLASSES from gluon import policy # from gluon._i18n import _ @@ -37,7 +40,7 @@ class PolicyHook(hooks.PecanHook): if state.request.method not in ('GET', 'POST', 'PUT', 'DELETE'): return - method = gluon_constants.ACTION_MAP[state.request.method] + method = generateMethod(state) path_info = state.request.path_info @@ -49,15 +52,17 @@ class PolicyHook(hooks.PecanHook): if not resource: return - action = "%s_%s" % (method, resource) + service = path_info.split("/")[2] + action = "%s:%s_%s" % (service, method, resource) gluon_context = state.request.context.get('gluon_context') policy.init() + target = generateTarget(state, service, resource) try: policy.enforce( - gluon_context, action, None) + gluon_context, action, target) except oslo_policy.PolicyNotAuthorized as e: raise webob.exc.HTTPForbidden(str(e)) @@ -65,3 +70,49 @@ class PolicyHook(hooks.PecanHook): # This method could be used for implementing access control # at the attribute level. return + + +# The policy enforce function requires target parameter +# oslo_policy doc descripbes target param as: "As much information about the +# object being operated on as possible" +# For delete and get, prefetch data from database and put tenant_id into target +# For post and put, get all user inputs from request body and put into target +def generateTarget(state, service, resource): + target = {} + method = state.request.method + if method in ('GET', 'DELETE') and state.arguments.args: + api_object_class = API_OBJECT_CLASSES[service][resource] + key = state.arguments.args[0] + obj = api_object_class.get_from_db(key) + tenant_id = obj.tenant_id + target['tenant_id'] = tenant_id + if method in ('POST', 'PUT'): + request_body = json.loads(state.request.body) + target = request_body + return target + + +# method could be 'get' or 'list' for the http get method +# If there is a key specified, method is 'get' +# If there is no key specified, method is 'list +def generateMethod(state): + method = state.request.method + if method == 'GET' and not state.arguments.args: + method = 'list' + else: + method = gluon_constants.ACTION_MAP[state.request.method] + return method + + +def findContrller(state): + path = state.request.path_info + pathList = path.split('/')[1:] + controller = state.app.root + resource = state.request.context.get('resource') + if pathList: + for item in pathList: + controller = getattr(controller, item) + if resource == item: + return controller + else: + return controller diff --git a/gluon/managers/manager_base.py b/gluon/managers/manager_base.py index 373f9c1..1e66273 100644 --- a/gluon/managers/manager_base.py +++ b/gluon/managers/manager_base.py @@ -110,6 +110,9 @@ class ApiManager(object): retry -= 1 return ret_val + # TODO(JinLi) This code now is hard-coded to create default interface when + # creating ports. Need to change the hardcoded code to support all types + # of YAML in future. def create_ports(self, api_class, values): ret_obj = api_class.create_in_db(values) # @@ -132,6 +135,7 @@ class ApiManager(object): name = 'default' data = {'id': values.get('id'), 'port_id': values.get('id'), + "tenant_id": values.get('tenant_id', ''), 'name': name, 'segmentation_type': 'none', 'segmentation_id': 0} diff --git a/gluon/models/base/base.yaml b/gluon/models/base/base.yaml index 8064831..f6e296e 100644 --- a/gluon/models/base/base.yaml +++ b/gluon/models/base/base.yaml @@ -6,17 +6,28 @@ objects: type: uuid primary: true description: "UUID of Object" + tenant_id: + type: uuid + required: true + description: "UUID of Tenant" name: type: string length: 64 description: "Descriptive name of Object" + policies: + create: + role: "rule:admin_or_owner" + delete: + role: "rule:admin_or_owner" + list: + role: "rule:admin" + get: + role: "rule:admin_or_owner" + update: + role: "rule:admin_or_owner" BasePort: extends: BaseObject attributes: - tenant_id: - type: uuid - required: true - description: "UUID of Tenant owning this Port" mac_address: type: string length: 18 @@ -108,6 +119,10 @@ objects: description: "Description of Service" BaseServiceBinding: attributes: + tenant_id: + type: uuid + required: true + description: "UUID of Tenant" interface_id: type: uuid required: true @@ -116,4 +131,15 @@ objects: service_id: type: uuid required: true - description: "Pointer to Service instance" \ No newline at end of file + description: "Pointer to Service instance" + policies: + create: + role: "rule:admin_or_owner" + delete: + role: "rule:admin_or_owner" + list: + role: "rule:admin" + get: + role: "rule:admin_or_owner" + update: + role: "rule:admin_or_owner" diff --git a/gluon/models/net-l3vpn/api.yaml b/gluon/models/net-l3vpn/api.yaml index 85c90ea..68750b2 100644 --- a/gluon/models/net-l3vpn/api.yaml +++ b/gluon/models/net-l3vpn/api.yaml @@ -73,6 +73,10 @@ objects: name: vpnafconfig plural_name: vpnafconfigs attributes: + tenant_id: + type: uuid + required: true + description: "UUID of Tenant" vrf_rt_value: required: true type: string @@ -95,6 +99,17 @@ objects: type: string length: 32 description: "Route target export policy" + policies: + create: + role: "rule:admin_or_owner" + delete: + role: "rule:admin_or_owner" + get: + role: "rule:admin_or_owner" + list: + role: "rule:admin" + update: + role: "rule:admin_or_owner" BGPPeering: api: name: bgppeering diff --git a/gluon/particleGenerator/ApiGenerator.py b/gluon/particleGenerator/ApiGenerator.py index 2991d17..45b8ce4 100644 --- a/gluon/particleGenerator/ApiGenerator.py +++ b/gluon/particleGenerator/ApiGenerator.py @@ -35,6 +35,7 @@ class MyData(object): ApiGenData = MyData() ApiGenData.svc_controllers = {} +API_OBJECT_CLASSES = {} class ProtonVersion(APIBase): @@ -170,6 +171,8 @@ class APIGenerator(object): return controller def create_api(self, root, service_name, db_models): + global API_OBJECT_CLASSES + API_OBJECT_CLASSES[service_name] = {} self.db_models = db_models self.service_name = service_name self.controllers = {} @@ -200,6 +203,11 @@ class APIGenerator(object): # api_name api_name = table_data['api']['plural_name'] + # Store each api_object_class created for the api. + # As in some situations, policy authorization will need to + # call an api_object_class to prefetch data + API_OBJECT_CLASSES[service_name][api_name] = api_object_class + # primary_key_type p_type, p_vals, p_fmt = self.get_primary_key_type(table_data) primary_key_type = self.translate_model_to_api_type(p_type, diff --git a/gluon/particleGenerator/PolicyGenerator.py b/gluon/particleGenerator/PolicyGenerator.py new file mode 100644 index 0000000..758af56 --- /dev/null +++ b/gluon/particleGenerator/PolicyGenerator.py @@ -0,0 +1,44 @@ +# Copyright 2017, AT&T +# +# 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_log._i18n import _ +from oslo_log import log as logging +from oslo_policy import policy as oslo_policy + +from gluon.particleGenerator import generator + + +LOG = logging.getLogger(__name__) + + +def policy_name(service, resource, action): + re = "%s:%s_%ss" + return re % (service, action, resource.lower()) + + +def generatePolicies(service_list): + policies = [] + for service in service_list: + model = generator.load_model_for_service(service) + generator.validate_policies(model) + for obj_name, obj_val in model['api_objects'].items(): + actions = obj_val.get('policies') + for action, rule in actions.items(): + name = policy_name(service, obj_name, action) + policy = oslo_policy.RuleDefault(name, rule.get('role')) + + LOG.info('%(n)s : %(r)s' % dict(n=name, r=rule.get('role'))) + + policies.append(policy) + return policies diff --git a/gluon/particleGenerator/generator.py b/gluon/particleGenerator/generator.py index 73afd35..947bc6a 100644 --- a/gluon/particleGenerator/generator.py +++ b/gluon/particleGenerator/generator.py @@ -44,6 +44,32 @@ def raise_obj_error(obj_name, format_str, val_tuple): raise_format_error("Object: %s, %s", (obj_name, str)) +# Check if policies are defined for each object and actions are valid. +# Does not verify the content of the policy for undefined rules and cyclic +# rules. The content of policies will be verified by calling the oslo_policy's +# load_rules funtions after all policies are registered. +def validate_policies(model): + '''Each api object should have policies defined''' + + allowed_actions = ['create', 'delete', 'get', 'list', 'update'] + for obj_name, obj_val in model['api_objects'].items(): + if 'policies' not in obj_val: + raise_obj_error(obj_name, + '%s has no policies defined.', + (obj_name)) + policies = obj_val.get('policies') + for action, policy in policies.items(): + if action not in allowed_actions: + raise_obj_error(obj_name, + 'In %s policies, %s is not a valid action.', + (obj_name, action)) + for action in allowed_actions: + if action not in policies: + raise_obj_error(obj_name, + 'In %s policies, action %s is not defined.', + (obj_name, action)) + + def validate_attributes(obj_name, obj, model): props = ['type', 'primary', 'description', 'required', 'length', 'values', 'format', 'min', 'max'] diff --git a/gluon/policies/__init__.py b/gluon/policies/__init__.py index da3dd43..233cf45 100644 --- a/gluon/policies/__init__.py +++ b/gluon/policies/__init__.py @@ -16,10 +16,16 @@ import itertools +from gluon.particleGenerator import generator +from gluon.particleGenerator import PolicyGenerator from gluon.policies import base +from gluon.policies import net_l3vpn def list_rules(): + service_list = generator.get_service_list() return itertools.chain( - base.list_rules() + base.list_rules(), + PolicyGenerator.generatePolicies(service_list) + # net_l3vpn.list_rules() ) diff --git a/gluon/policies/base.py b/gluon/policies/base.py index e51905e..cad888d 100644 --- a/gluon/policies/base.py +++ b/gluon/policies/base.py @@ -16,6 +16,17 @@ from oslo_policy import policy + +RULE_CONTEXT_IS_ADMIN = 'rule:context_is_admin' +RULE_OWNER = 'rule:owner' +RULE_ADMIN_OR_OWNER = 'rule:admin_or_owner' +RULE_CONTEXT_IS_ADVSVC = 'rule:context_is_advsvc' +RULE_ADMIN_OR_NETWORK_OWNER = 'rule:admin_or_network_owner' +RULE_ADMIN_OWNER_OR_NETWORK_OWNER = 'rule:admin_owner_or_network_owner' +RULE_ADMIN_ONLY = 'rule:admin_only' +RULE_REGULAR_USER = 'rule:regular_user' +RULE_DEFAULT = 'rule:default' + rules = [ policy.RuleDefault('context_is_admin', 'role:admin'), policy.RuleDefault('owner', 'tenant_id:%(tenant_id)s'), diff --git a/gluon/policies/net_l3vpn.py b/gluon/policies/net_l3vpn.py new file mode 100644 index 0000000..937780b --- /dev/null +++ b/gluon/policies/net_l3vpn.py @@ -0,0 +1,214 @@ +# Copyright (c) 2016 OpenStack Foundation. +# 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 gluon.policies import base + + +# TODO(JinLi) This file is NOT used, it is an example of moving policy to code. +# Unlike other Openstack projects whose api has a fix set of restControllers, +# Gluon dynamically generates its restControllers from yaml files. If Gluon +# follows the policy in code approach, Gluon users will need to modify source +# code by adding similar files like this one. And then call the list_rules +# function inside the gluon.policies.__init__.py +# +# Gluon takes a different approach by defining policies inside the yaml file of +# a model, so that users do not need to modify any source code +# +# If user prefers to use plicy in code, they can use this file. And create +# similar file for new service. +net_l3vpn_policies = [ + policy.RuleDefault( + name='net-l3vpn:create_dataplanetunnels', + check_str=base.RULE_ADMIN_OR_OWNER, + description='net-l3vpn policy' + ), + policy.RuleDefault( + name='net-l3vpn:get_dataplanetunnels', + check_str=base.RULE_ADMIN_OR_OWNER, + description='net-l3vpn policy' + ), + policy.RuleDefault( + name='net-l3vpn:update_dataplanetunnels', + check_str=base.RULE_ADMIN_OR_OWNER, + description='net-l3vpn policy' + ), + policy.RuleDefault( + name='net-l3vpn:get_one_dataplanetunnels', + check_str=base.RULE_ADMIN_OR_OWNER, + description='net-l3vpn policy' + ), + policy.RuleDefault( + name='net-l3vpn:delete_dataplanetunnels', + check_str=base.RULE_ADMIN_OR_OWNER, + description='net-l3vpn policy' + ), + policy.RuleDefault( + name='net-l3vpn:create_bgppeerings', + check_str=base.RULE_ADMIN_OR_OWNER, + description='net-l3vpn policy' + ), + policy.RuleDefault( + name='net-l3vpn:get_bgppeerings', + check_str=base.RULE_ADMIN_OR_OWNER, + description='net-l3vpn policy' + ), + policy.RuleDefault( + name='net-l3vpn:update_bgppeerings', + check_str=base.RULE_ADMIN_OR_OWNER, + description='net-l3vpn policy' + ), + policy.RuleDefault( + name='net-l3vpn:get_one_bgppeerings', + check_str=base.RULE_ADMIN_OR_OWNER, + description='net-l3vpn policy' + ), + policy.RuleDefault( + name='net-l3vpn:delete_bgppeerings', + check_str=base.RULE_ADMIN_OR_OWNER, + description='net-l3vpn policy' + ), + policy.RuleDefault( + name='net-l3vpn:create_vpnafconfigs', + check_str=base.RULE_ADMIN_OR_OWNER, + description='net-l3vpn policy' + ), + policy.RuleDefault( + name='net-l3vpn:get_vpnafconfigs', + check_str=base.RULE_ADMIN_OR_OWNER, + description='net-l3vpn policy' + ), + policy.RuleDefault( + name='net-l3vpn:update_vpnafconfigs', + check_str=base.RULE_ADMIN_OR_OWNER, + description='net-l3vpn policy' + ), + policy.RuleDefault( + name='net-l3vpn:get_one_vpnafconfigs', + check_str=base.RULE_ADMIN_OR_OWNER, + description='net-l3vpn policy' + ), + policy.RuleDefault( + name='net-l3vpn:delete_vpnafconfigs', + check_str=base.RULE_ADMIN_OR_OWNER, + description='net-l3vpn policy' + ), + policy.RuleDefault( + name='net-l3vpn:create_vpnservices', + check_str=base.RULE_ADMIN_OR_OWNER, + description='net-l3vpn policy' + ), + policy.RuleDefault( + name='net-l3vpn:get_vpnservices', + check_str=base.RULE_ADMIN_OR_OWNER, + description='net-l3vpn policy' + ), + policy.RuleDefault( + name='net-l3vpn:update_vpnservices', + check_str=base.RULE_ADMIN_OR_OWNER, + description='net-l3vpn policy' + ), + policy.RuleDefault( + name='net-l3vpn:get_one_vpnservices', + check_str=base.RULE_ADMIN_OR_OWNER, + description='net-l3vpn policy' + ), + policy.RuleDefault( + name='net-l3vpn:delete_vpnservices', + check_str=base.RULE_ADMIN_OR_OWNER, + description='net-l3vpn policy' + ), + policy.RuleDefault( + name='net-l3vpn:create_interfaces', + check_str=base.RULE_ADMIN_OR_OWNER, + description='net-l3vpn policy' + ), + policy.RuleDefault( + name='net-l3vpn:get_interfaces', + check_str=base.RULE_ADMIN_OR_OWNER, + description='net-l3vpn policy' + ), + policy.RuleDefault( + name='net-l3vpn:update_interfaces', + check_str=base.RULE_ADMIN_OR_OWNER, + description='net-l3vpn policy' + ), + policy.RuleDefault( + name='net-l3vpn:get_one_interfaces', + check_str=base.RULE_ADMIN_OR_OWNER, + description='net-l3vpn policy' + ), + policy.RuleDefault( + name='net-l3vpn:delete_interfaces', + check_str=base.RULE_ADMIN_OR_OWNER, + description='net-l3vpn policy' + ), + policy.RuleDefault( + name='net-l3vpn:create_vpnbindings', + check_str=base.RULE_ADMIN_OR_OWNER, + description='net-l3vpn policy' + ), + policy.RuleDefault( + name='net-l3vpn:get_vpnbindings', + check_str=base.RULE_ADMIN_OR_OWNER, + description='net-l3vpn policy' + ), + policy.RuleDefault( + name='net-l3vpn:update_vpnbindings', + check_str=base.RULE_ADMIN_OR_OWNER, + description='net-l3vpn policy' + ), + policy.RuleDefault( + name='net-l3vpn:get_one_vpnbindings', + check_str=base.RULE_ADMIN_OR_OWNER, + description='net-l3vpn policy' + ), + policy.RuleDefault( + name='net-l3vpn:delete_vpnbindings', + check_str=base.RULE_ADMIN_OR_OWNER, + description='net-l3vpn policy' + ), + policy.RuleDefault( + name='net-l3vpn:create_ports', + check_str=base.RULE_ADMIN_OR_OWNER, + description='net-l3vpn policy' + ), + policy.RuleDefault( + name='net-l3vpn:get_ports', + check_str=base.RULE_ADMIN_OR_OWNER, + description='net-l3vpn policy' + ), + policy.RuleDefault( + name='net-l3vpn:update_ports', + check_str=base.RULE_ADMIN_OR_OWNER, + description='net-l3vpn policy' + ), + policy.RuleDefault( + name='net-l3vpn:get_one_ports', + check_str=base.RULE_ADMIN_OR_OWNER, + description='net-l3vpn policy' + ), + policy.RuleDefault( + name='net-l3vpn:delete_ports', + check_str=base.RULE_ADMIN_OR_OWNER, + description='net-l3vpn policy' + ) +] + + +def list_rules(): + return net_l3vpn_policies