Policy in code
This patch introduces the implementation for registering default policy rules in code. Default rules are defined under cloudkitty.common.policies. Each API's policies are defined in a sub-folder under that path and __init__.py contains all the default policies in code which are registered in the ``init`` enforcer function in cloudkitty/common/policy.py. This commit does the following: - Creates the ``policies`` module that contains all the default policies in code. - Adds the base policy rules into code (context_is_admin, admin_or_owner and default rules). - Add policies in code for current APIs - Add a tox env to generate default policy sample file - Delete policy.json from repo as policies in code will be used. Change-Id: I257e8cefc2b699fc979c717531cd9ba77233d94b Implements: blueprint policy-in-code
This commit is contained in:
parent
43e1999e9d
commit
7eca672645
1
.gitignore
vendored
1
.gitignore
vendored
@ -25,6 +25,7 @@ cloudkitty.egg-info
|
||||
|
||||
# Configuration file
|
||||
etc/cloudkitty/cloudkitty.conf.sample
|
||||
etc/cloudkitty/policy.yaml.sample
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
|
@ -39,7 +39,7 @@ class MappingController(rest.RestController):
|
||||
|
||||
:param service: Name of the service to filter on.
|
||||
"""
|
||||
policy.enforce(pecan.request.context, 'collector:get_mapping', {})
|
||||
policy.authorize(pecan.request.context, 'collector:get_mapping', {})
|
||||
try:
|
||||
mapping = self._db.get_mapping(service)
|
||||
return collector_models.ServiceToCollectorMapping(
|
||||
@ -55,7 +55,7 @@ class MappingController(rest.RestController):
|
||||
:param collector: Filter on the collector name.
|
||||
:return: Service to collector mappings collection.
|
||||
"""
|
||||
policy.enforce(pecan.request.context, 'collector:list_mappings', {})
|
||||
policy.authorize(pecan.request.context, 'collector:list_mappings', {})
|
||||
mappings = [collector_models.ServiceToCollectorMapping(
|
||||
**mapping.as_dict())
|
||||
for mapping in self._db.list_mappings(collector)]
|
||||
@ -71,7 +71,7 @@ class MappingController(rest.RestController):
|
||||
:param collector: Name of the collector to apply mapping on.
|
||||
:param service: Name of the service to apply mapping on.
|
||||
"""
|
||||
policy.enforce(pecan.request.context, 'collector:manage_mapping', {})
|
||||
policy.authorize(pecan.request.context, 'collector:manage_mapping', {})
|
||||
new_mapping = self._db.set_mapping(service, collector)
|
||||
return collector_models.ServiceToCollectorMapping(
|
||||
service=new_mapping.service,
|
||||
@ -85,7 +85,7 @@ class MappingController(rest.RestController):
|
||||
|
||||
:param service: Name of the service to filter on.
|
||||
"""
|
||||
policy.enforce(pecan.request.context, 'collector:manage_mapping', {})
|
||||
policy.authorize(pecan.request.context, 'collector:manage_mapping', {})
|
||||
try:
|
||||
self._db.delete_mapping(service)
|
||||
except db_api.NoSuchMapping as e:
|
||||
@ -105,7 +105,7 @@ class CollectorStateController(rest.RestController):
|
||||
:param name: Name of the collector.
|
||||
:return: State of the collector.
|
||||
"""
|
||||
policy.enforce(pecan.request.context, 'collector:get_state', {})
|
||||
policy.authorize(pecan.request.context, 'collector:get_state', {})
|
||||
enabled = self._db.get_state('collector_{}'.format(name))
|
||||
collector = collector_models.CollectorInfos(name=name,
|
||||
enabled=enabled)
|
||||
@ -121,7 +121,7 @@ class CollectorStateController(rest.RestController):
|
||||
:param infos: New state informations of the collector.
|
||||
:return: State of the collector.
|
||||
"""
|
||||
policy.enforce(pecan.request.context, 'collector:update_state', {})
|
||||
policy.authorize(pecan.request.context, 'collector:update_state', {})
|
||||
enabled = self._db.set_state('collector_{}'.format(name),
|
||||
infos.enabled)
|
||||
collector = collector_models.CollectorInfos(name=name,
|
||||
|
@ -44,7 +44,7 @@ class ServiceInfoController(rest.RestController):
|
||||
|
||||
:return: List of every services.
|
||||
"""
|
||||
policy.enforce(pecan.request.context, 'info:list_services_info', {})
|
||||
policy.authorize(pecan.request.context, 'info:list_services_info', {})
|
||||
services_info_list = []
|
||||
for service, metadata in METADATA.items():
|
||||
info = metadata.copy()
|
||||
@ -60,7 +60,7 @@ class ServiceInfoController(rest.RestController):
|
||||
|
||||
:param service_name: name of the service.
|
||||
"""
|
||||
policy.enforce(pecan.request.context, 'info:get_service_info', {})
|
||||
policy.authorize(pecan.request.context, 'info:get_service_info', {})
|
||||
try:
|
||||
info = METADATA[service_name].copy()
|
||||
info['service_id'] = service_name
|
||||
@ -81,7 +81,7 @@ class InfoController(rest.RestController):
|
||||
})
|
||||
def config(self):
|
||||
"""Return current configuration."""
|
||||
policy.enforce(pecan.request.context, 'info:get_config', {})
|
||||
policy.authorize(pecan.request.context, 'info:get_config', {})
|
||||
info = {}
|
||||
info["collect"] = ck_utils.get_metrics_conf(CONF.collect.metrics_conf)
|
||||
return info
|
||||
|
@ -63,7 +63,7 @@ class ModulesController(rest.RestController, RatingModulesMixin):
|
||||
def route(self, *args):
|
||||
route = args[0]
|
||||
if route.startswith('/v1/module_config'):
|
||||
policy.enforce(pecan.request.context, 'rating:module_config', {})
|
||||
policy.authorize(pecan.request.context, 'rating:module_config', {})
|
||||
|
||||
super(ModulesController, self).route(*args)
|
||||
|
||||
@ -73,7 +73,7 @@ class ModulesController(rest.RestController, RatingModulesMixin):
|
||||
|
||||
:return: name of every loaded modules.
|
||||
"""
|
||||
policy.enforce(pecan.request.context, 'rating:list_modules', {})
|
||||
policy.authorize(pecan.request.context, 'rating:list_modules', {})
|
||||
|
||||
modules_list = []
|
||||
lock = lockutils.lock('rating-modules')
|
||||
@ -92,7 +92,7 @@ class ModulesController(rest.RestController, RatingModulesMixin):
|
||||
|
||||
:return: CloudKittyModule
|
||||
"""
|
||||
policy.enforce(pecan.request.context, 'rating:get_module', {})
|
||||
policy.authorize(pecan.request.context, 'rating:get_module', {})
|
||||
|
||||
try:
|
||||
lock = lockutils.lock('rating-modules')
|
||||
@ -114,7 +114,7 @@ class ModulesController(rest.RestController, RatingModulesMixin):
|
||||
:param module_id: name of the module to modify
|
||||
:param module: CloudKittyModule object describing the new desired state
|
||||
"""
|
||||
policy.enforce(pecan.request.context, 'rating:update_module', {})
|
||||
policy.authorize(pecan.request.context, 'rating:update_module', {})
|
||||
|
||||
try:
|
||||
lock = lockutils.lock('rating-modules')
|
||||
@ -194,7 +194,7 @@ class RatingController(rest.RestController):
|
||||
:param res_data: List of resource descriptions.
|
||||
:return: Total price for these descriptions.
|
||||
"""
|
||||
policy.enforce(pecan.request.context, 'rating:quote', {})
|
||||
policy.authorize(pecan.request.context, 'rating:quote', {})
|
||||
|
||||
client = pecan.request.rpc_client.prepare(namespace='rating')
|
||||
res_dict = {}
|
||||
@ -212,7 +212,7 @@ class RatingController(rest.RestController):
|
||||
"""Trigger a rating module list reload.
|
||||
|
||||
"""
|
||||
policy.enforce(pecan.request.context, 'rating:module_config', {})
|
||||
policy.authorize(pecan.request.context, 'rating:module_config', {})
|
||||
self.modules.reload_extensions()
|
||||
self.module_config.reload_extensions()
|
||||
self.module_config.expose_modules()
|
||||
|
@ -46,7 +46,7 @@ class ReportController(rest.RestController):
|
||||
"""Return the list of rated tenants.
|
||||
|
||||
"""
|
||||
policy.enforce(pecan.request.context, 'report:list_tenants', {})
|
||||
policy.authorize(pecan.request.context, 'report:list_tenants', {})
|
||||
|
||||
if not begin:
|
||||
begin = ck_utils.get_month_start()
|
||||
@ -78,8 +78,8 @@ class ReportController(rest.RestController):
|
||||
else:
|
||||
tenant_context = pecan.request.context.tenant
|
||||
tenant_id = tenant_context if not tenant_id else tenant_id
|
||||
policy.enforce(pecan.request.context, 'report:get_total',
|
||||
{"tenant_id": tenant_id})
|
||||
policy.authorize(pecan.request.context, 'report:get_total',
|
||||
{"tenant_id": tenant_id})
|
||||
|
||||
storage = pecan.request.storage_backend
|
||||
# FIXME(sheeprine): We should filter on user id.
|
||||
@ -114,8 +114,8 @@ class ReportController(rest.RestController):
|
||||
else:
|
||||
tenant_context = pecan.request.context.tenant
|
||||
tenant_id = tenant_context if not tenant_id else tenant_id
|
||||
policy.enforce(pecan.request.context, 'report:get_summary',
|
||||
{"tenant_id": tenant_id})
|
||||
policy.authorize(pecan.request.context, 'report:get_summary',
|
||||
{"tenant_id": tenant_id})
|
||||
storage = pecan.request.storage_backend
|
||||
|
||||
summarymodels = []
|
||||
|
@ -48,7 +48,7 @@ class DataFramesController(rest.RestController):
|
||||
:return: Collection of DataFrame objects.
|
||||
"""
|
||||
|
||||
policy.enforce(pecan.request.context, 'storage:list_data_frames', {})
|
||||
policy.authorize(pecan.request.context, 'storage:list_data_frames', {})
|
||||
|
||||
if not begin:
|
||||
begin = ck_utils.get_month_start()
|
||||
|
34
cloudkitty/common/policies/__init__.py
Normal file
34
cloudkitty/common/policies/__init__.py
Normal file
@ -0,0 +1,34 @@
|
||||
# Copyright 2017 GohighSec.
|
||||
# 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 cloudkitty.common.policies import base
|
||||
from cloudkitty.common.policies import collector
|
||||
from cloudkitty.common.policies import info
|
||||
from cloudkitty.common.policies import rating
|
||||
from cloudkitty.common.policies import report
|
||||
from cloudkitty.common.policies import storage
|
||||
|
||||
|
||||
def list_rules():
|
||||
return itertools.chain(
|
||||
base.list_rules(),
|
||||
collector.list_rules(),
|
||||
info.list_rules(),
|
||||
rating.list_rules(),
|
||||
report.list_rules(),
|
||||
storage.list_rules()
|
||||
)
|
36
cloudkitty/common/policies/base.py
Normal file
36
cloudkitty/common/policies/base.py
Normal file
@ -0,0 +1,36 @@
|
||||
# Copyright 2017 GohighSec.
|
||||
# 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
|
||||
|
||||
RULE_ADMIN_OR_OWNER = 'rule:admin_or_owner'
|
||||
ROLE_ADMIN = 'role:admin'
|
||||
UNPROTECTED = ''
|
||||
|
||||
rules = [
|
||||
policy.RuleDefault(
|
||||
name='context_is_admin',
|
||||
check_str='role:admin'),
|
||||
policy.RuleDefault(
|
||||
name='admin_or_owner',
|
||||
check_str='is_admin:True or tenant:%(tenant_id)s'),
|
||||
policy.RuleDefault(
|
||||
name='default',
|
||||
check_str=UNPROTECTED)
|
||||
]
|
||||
|
||||
|
||||
def list_rules():
|
||||
return rules
|
57
cloudkitty/common/policies/collector.py
Normal file
57
cloudkitty/common/policies/collector.py
Normal file
@ -0,0 +1,57 @@
|
||||
# Copyright 2017 GohighSec.
|
||||
# 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 cloudkitty.common.policies import base
|
||||
|
||||
collector_policies = [
|
||||
policy.DocumentedRuleDefault(
|
||||
name='collector:list_mappings',
|
||||
check_str=base.ROLE_ADMIN,
|
||||
description='Return the list of every services mapped to a collector.',
|
||||
operations=[{'path': '/v1/collector/mappings',
|
||||
'method': 'LIST'}]),
|
||||
policy.DocumentedRuleDefault(
|
||||
name='collector:get_mapping',
|
||||
check_str=base.ROLE_ADMIN,
|
||||
description='Return a service to collector mapping.',
|
||||
operations=[{'path': '/v1/collector/mappings/{service_id}',
|
||||
'method': 'GET'}]),
|
||||
policy.DocumentedRuleDefault(
|
||||
name='collector:manage_mapping',
|
||||
check_str=base.ROLE_ADMIN,
|
||||
description='Manage a service to collector mapping.',
|
||||
operations=[{'path': '/v1/collector/mappings',
|
||||
'method': 'POST'},
|
||||
{'path': '/v1/collector/mappings/{service_id}',
|
||||
'method': 'DELETE'}]),
|
||||
policy.DocumentedRuleDefault(
|
||||
name='collector:get_state',
|
||||
check_str=base.ROLE_ADMIN,
|
||||
description='Query the enable state of a collector.',
|
||||
operations=[{'path': '/v1/collector/states/{collector_id}',
|
||||
'method': 'GET'}]),
|
||||
policy.DocumentedRuleDefault(
|
||||
name='collector:update_state',
|
||||
check_str=base.ROLE_ADMIN,
|
||||
description='Set the enable state of a collector.',
|
||||
operations=[{'path': '/v1/collector/states/{collector_id}',
|
||||
'method': 'PUT'}])
|
||||
]
|
||||
|
||||
|
||||
def list_rules():
|
||||
return collector_policies
|
43
cloudkitty/common/policies/info.py
Normal file
43
cloudkitty/common/policies/info.py
Normal file
@ -0,0 +1,43 @@
|
||||
# Copyright 2017 GohighSec.
|
||||
# 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 cloudkitty.common.policies import base
|
||||
|
||||
info_policies = [
|
||||
policy.DocumentedRuleDefault(
|
||||
name='info:list_services_info',
|
||||
check_str=base.UNPROTECTED,
|
||||
description='List available services information in Cloudkitty.',
|
||||
operations=[{'path': '/v1/info/services',
|
||||
'method': 'LIST'}]),
|
||||
policy.DocumentedRuleDefault(
|
||||
name='info:get_service_info',
|
||||
check_str=base.UNPROTECTED,
|
||||
description='Get specified service information.',
|
||||
operations=[{'path': '/v1/info/services/{service_id}',
|
||||
'method': 'GET'}]),
|
||||
policy.DocumentedRuleDefault(
|
||||
name='info:get_config',
|
||||
check_str=base.UNPROTECTED,
|
||||
description='Get current configuration in Cloudkitty.',
|
||||
operations=[{'path': '/v1/info/config',
|
||||
'method': 'GET'}])
|
||||
]
|
||||
|
||||
|
||||
def list_rules():
|
||||
return info_policies
|
56
cloudkitty/common/policies/rating.py
Normal file
56
cloudkitty/common/policies/rating.py
Normal file
@ -0,0 +1,56 @@
|
||||
# Copyright 2017 GohighSec.
|
||||
# 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 cloudkitty.common.policies import base
|
||||
|
||||
rating_policies = [
|
||||
policy.DocumentedRuleDefault(
|
||||
name='rating:list_modules',
|
||||
check_str=base.ROLE_ADMIN,
|
||||
description='Reture the list of loaded modules in Cloudkitty.',
|
||||
operations=[{'path': '/v1/rating/modules',
|
||||
'method': 'LIST'}]),
|
||||
policy.DocumentedRuleDefault(
|
||||
name='rating:get_module',
|
||||
check_str=base.ROLE_ADMIN,
|
||||
description='Get specified module.',
|
||||
operations=[{'path': '/v1/rating/modules/{module_id}',
|
||||
'method': 'GET'}]),
|
||||
policy.DocumentedRuleDefault(
|
||||
name='rating:update_module',
|
||||
check_str=base.ROLE_ADMIN,
|
||||
description='Change the state and priority of a module.',
|
||||
operations=[{'path': '/v1/rating/modules/{module_id}',
|
||||
'method': 'PUT'}]),
|
||||
policy.DocumentedRuleDefault(
|
||||
name='rating:quote',
|
||||
check_str=base.UNPROTECTED,
|
||||
description='Get an instant quote based on multiple resource '
|
||||
'descriptions.',
|
||||
operations=[{'path': '/v1/rating/quote',
|
||||
'method': 'POST'}]),
|
||||
policy.DocumentedRuleDefault(
|
||||
name='rating:module_config',
|
||||
check_str=base.ROLE_ADMIN,
|
||||
description='Trigger a rating module list reload.',
|
||||
operations=[{'path': '/v1/rating/reload_modules',
|
||||
'method': 'GET'}])
|
||||
]
|
||||
|
||||
|
||||
def list_rules():
|
||||
return rating_policies
|
43
cloudkitty/common/policies/report.py
Normal file
43
cloudkitty/common/policies/report.py
Normal file
@ -0,0 +1,43 @@
|
||||
# Copyright 2017 GohighSec.
|
||||
# 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 cloudkitty.common.policies import base
|
||||
|
||||
report_policies = [
|
||||
policy.DocumentedRuleDefault(
|
||||
name='report:list_tenants',
|
||||
check_str=base.ROLE_ADMIN,
|
||||
description='Return the list of rated tenants.',
|
||||
operations=[{'path': '/v1/report/tenants',
|
||||
'method': 'GET'}]),
|
||||
policy.DocumentedRuleDefault(
|
||||
name='report:get_summary',
|
||||
check_str=base.RULE_ADMIN_OR_OWNER,
|
||||
description='Return the summary to pay for a given period.',
|
||||
operations=[{'path': '/v1/report/summary',
|
||||
'method': 'GET'}]),
|
||||
policy.DocumentedRuleDefault(
|
||||
name='report:get_total',
|
||||
check_str=base.RULE_ADMIN_OR_OWNER,
|
||||
description='Return the amount to pay for a given period.',
|
||||
operations=[{'path': '/v1/report/total',
|
||||
'method': 'GET'}])
|
||||
]
|
||||
|
||||
|
||||
def list_rules():
|
||||
return report_policies
|
32
cloudkitty/common/policies/storage.py
Normal file
32
cloudkitty/common/policies/storage.py
Normal file
@ -0,0 +1,32 @@
|
||||
# Copyright 2017 GohighSec.
|
||||
# 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 cloudkitty.common.policies import base
|
||||
|
||||
storage_policies = [
|
||||
policy.DocumentedRuleDefault(
|
||||
name='storage:list_data_frames',
|
||||
check_str=base.UNPROTECTED,
|
||||
description='Return a list of rated resources for a time period '
|
||||
'and a tenant.',
|
||||
operations=[{'path': '/v1/storage/dataframes',
|
||||
'method': 'GET'}])
|
||||
]
|
||||
|
||||
|
||||
def list_rules():
|
||||
return storage_policies
|
@ -15,13 +15,28 @@
|
||||
|
||||
# Borrowed from cinder (cinder/policy.py)
|
||||
|
||||
import copy
|
||||
import sys
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
from oslo_policy import opts as policy_opts
|
||||
from oslo_policy import policy
|
||||
from oslo_utils import excutils
|
||||
import six
|
||||
|
||||
from cloudkitty.common import policies
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
CONF = cfg.CONF
|
||||
policy_opts.set_defaults(cfg.CONF, 'policy.json')
|
||||
|
||||
_ENFORCER = None
|
||||
# oslo_policy will read the policy configuration file again when the file
|
||||
# is changed in runtime so the old policy rules will be saved to
|
||||
# saved_file_rules and used to compare with new rules to determine the
|
||||
# rules whether were updated.
|
||||
saved_file_rules = []
|
||||
|
||||
|
||||
# TODO(gpocentek): provide a proper parent class to handle such exceptions
|
||||
@ -37,13 +52,37 @@ class PolicyNotAuthorized(Exception):
|
||||
return six.text_type(self.msg)
|
||||
|
||||
|
||||
def reset():
|
||||
global _ENFORCER
|
||||
if _ENFORCER:
|
||||
_ENFORCER.clear()
|
||||
_ENFORCER = None
|
||||
|
||||
|
||||
def init():
|
||||
global _ENFORCER
|
||||
global saved_file_rules
|
||||
if not _ENFORCER:
|
||||
_ENFORCER = policy.Enforcer(CONF)
|
||||
register_rules(_ENFORCER)
|
||||
|
||||
# Only the rules which are loaded from file may be changed.
|
||||
current_file_rules = _ENFORCER.file_rules
|
||||
current_file_rules = _serialize_rules(current_file_rules)
|
||||
|
||||
# Checks whether the rules are updated in the runtime
|
||||
if saved_file_rules != current_file_rules:
|
||||
saved_file_rules = copy.deepcopy(current_file_rules)
|
||||
|
||||
|
||||
def enforce(context, action, target):
|
||||
def _serialize_rules(rules):
|
||||
"""Serialize all the Rule object as string."""
|
||||
result = [(rule_name, str(rule))
|
||||
for rule_name, rule in rules.items()]
|
||||
return sorted(result, key=lambda rule: rule[0])
|
||||
|
||||
|
||||
def authorize(context, action, target):
|
||||
"""Verifies that the action is valid on the target in this context.
|
||||
|
||||
:param context: cloudkitty context
|
||||
@ -65,10 +104,20 @@ def enforce(context, action, target):
|
||||
|
||||
init()
|
||||
|
||||
return _ENFORCER.enforce(action, target, context.to_dict(),
|
||||
do_raise=True,
|
||||
exc=PolicyNotAuthorized,
|
||||
action=action)
|
||||
try:
|
||||
return _ENFORCER.authorize(action, target, context.to_dict(),
|
||||
do_raise=True,
|
||||
exc=PolicyNotAuthorized,
|
||||
action=action)
|
||||
|
||||
except policy.PolicyNotRegistered:
|
||||
with excutils.save_and_reraise_exception():
|
||||
LOG.exception('Policy not registered')
|
||||
except Exception:
|
||||
with excutils.save_and_reraise_exception():
|
||||
LOG.error('Policy check for %(action)s failed with credentials '
|
||||
'%(credentials)s',
|
||||
{'action': action, 'credentials': context.to_dict()})
|
||||
|
||||
|
||||
def check_is_admin(roles):
|
||||
@ -87,4 +136,28 @@ def check_is_admin(roles):
|
||||
target = {'project_id': ''}
|
||||
credentials = {'roles': roles}
|
||||
|
||||
return _ENFORCER.enforce('context_is_admin', target, credentials)
|
||||
return _ENFORCER.authorize('context_is_admin', target, credentials)
|
||||
|
||||
|
||||
def register_rules(enforcer):
|
||||
enforcer.register_defaults(policies.list_rules())
|
||||
|
||||
|
||||
def get_enforcer():
|
||||
# This method is for use by oslopolicy CLI scripts. Those scripts need the
|
||||
# 'output-file' and 'namespace' options, but having those in sys.argv means
|
||||
# loading the Cloudkitty config options will fail as those are not expected
|
||||
# to be present. So we pass in an arg list with those stripped out.
|
||||
conf_args = []
|
||||
# Start at 1 because cfg.CONF expects the equivalent of sys.argv[1:]
|
||||
i = 1
|
||||
while i < len(sys.argv):
|
||||
if sys.argv[i].strip('-') in ['namespace', 'output-file']:
|
||||
i += 2
|
||||
continue
|
||||
conf_args.append(sys.argv[i])
|
||||
i += 1
|
||||
|
||||
cfg.CONF(conf_args, project='cloudkitty')
|
||||
init()
|
||||
return _ENFORCER
|
||||
|
@ -142,7 +142,7 @@ class RatingRestControllerBase(rest.RestController):
|
||||
@pecan.expose()
|
||||
def _route(self, args, request):
|
||||
try:
|
||||
policy.enforce(request.context, 'rating:module_config', {})
|
||||
policy.authorize(request.context, 'rating:module_config', {})
|
||||
except policy.PolicyNotAuthorized as e:
|
||||
pecan.abort(403, six.text_type(e))
|
||||
|
||||
|
122
cloudkitty/tests/test_policy.py
Normal file
122
cloudkitty/tests/test_policy.py
Normal file
@ -0,0 +1,122 @@
|
||||
# Copyright (c) 2017 GohighSec.
|
||||
# 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 os.path
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_config import fixture as config_fixture
|
||||
from oslo_context import context
|
||||
from oslo_policy import policy as oslo_policy
|
||||
|
||||
from cloudkitty.common import policy
|
||||
from cloudkitty import tests
|
||||
from cloudkitty import utils
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
class PolicyFileTestCase(tests.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(PolicyFileTestCase, self).setUp()
|
||||
self.context = context.RequestContext('fake', 'fake', roles=['member'])
|
||||
self.target = {}
|
||||
self.fixture = self.useFixture(config_fixture.Config(CONF))
|
||||
self.addCleanup(policy.reset)
|
||||
CONF(args=[], project='cloudkitty', default_config_files=[])
|
||||
|
||||
def test_modified_policy_reloads(self):
|
||||
with utils.tempdir() as tmpdir:
|
||||
tmpfilename = os.path.join(tmpdir, 'policy')
|
||||
self.fixture.config(policy_file=tmpfilename, group='oslo_policy')
|
||||
rule = oslo_policy.RuleDefault('example:test', "")
|
||||
policy.reset()
|
||||
policy.init()
|
||||
policy._ENFORCER.register_defaults([rule])
|
||||
|
||||
action = "example:test"
|
||||
with open(tmpfilename, "w") as policyfile:
|
||||
policyfile.write('{"example:test": ""}')
|
||||
policy.authorize(self.context, action, self.target)
|
||||
with open(tmpfilename, "w") as policyfile:
|
||||
policyfile.write('{"example:test": "!"}')
|
||||
policy._ENFORCER.load_rules(True)
|
||||
self.assertRaises(policy.PolicyNotAuthorized,
|
||||
policy.authorize,
|
||||
self.context, action, self.target)
|
||||
|
||||
|
||||
class PolicyTestCase(tests.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(PolicyTestCase, self).setUp()
|
||||
rules = [
|
||||
oslo_policy.RuleDefault("true", '@'),
|
||||
oslo_policy.RuleDefault("test:allowed", '@'),
|
||||
oslo_policy.RuleDefault("test:denied", "!"),
|
||||
oslo_policy.RuleDefault("test:early_and_fail", "! and @"),
|
||||
oslo_policy.RuleDefault("test:early_or_success", "@ or !"),
|
||||
oslo_policy.RuleDefault("test:lowercase_admin",
|
||||
"role:admin"),
|
||||
oslo_policy.RuleDefault("test:uppercase_admin",
|
||||
"role:ADMIN"),
|
||||
]
|
||||
CONF(args=[], project='cloudkitty', default_config_files=[])
|
||||
# before a policy rule can be used, its default has to be registered.
|
||||
policy.reset()
|
||||
policy.init()
|
||||
policy._ENFORCER.register_defaults(rules)
|
||||
self.context = context.RequestContext('fake',
|
||||
'fake',
|
||||
roles=['member'])
|
||||
self.target = {}
|
||||
self.addCleanup(policy.reset)
|
||||
|
||||
def test_enforce_nonexistent_action_throws(self):
|
||||
action = "test:noexist"
|
||||
self.assertRaises(oslo_policy.PolicyNotRegistered, policy.authorize,
|
||||
self.context, action, self.target)
|
||||
|
||||
def test_enforce_bad_action_throws(self):
|
||||
action = "test:denied"
|
||||
self.assertRaises(policy.PolicyNotAuthorized, policy.authorize,
|
||||
self.context, action, self.target)
|
||||
|
||||
def test_enforce_bad_action_noraise(self):
|
||||
action = "test:denied"
|
||||
self.assertRaises(policy.PolicyNotAuthorized, policy.authorize,
|
||||
self.context, action, self.target)
|
||||
|
||||
def test_enforce_good_action(self):
|
||||
action = "test:allowed"
|
||||
result = policy.authorize(self.context, action, self.target)
|
||||
self.assertTrue(result)
|
||||
|
||||
def test_early_AND_authorization(self):
|
||||
action = "test:early_and_fail"
|
||||
self.assertRaises(policy.PolicyNotAuthorized, policy.authorize,
|
||||
self.context, action, self.target)
|
||||
|
||||
def test_early_OR_authorization(self):
|
||||
action = "test:early_or_success"
|
||||
policy.authorize(self.context, action, self.target)
|
||||
|
||||
def test_ignore_case_role_check(self):
|
||||
lowercase_action = "test:lowercase_admin"
|
||||
uppercase_action = "test:uppercase_admin"
|
||||
admin_context = context.RequestContext('admin',
|
||||
'fake',
|
||||
roles=['AdMiN'])
|
||||
policy.authorize(admin_context, lowercase_action, self.target)
|
||||
policy.authorize(admin_context, uppercase_action, self.target)
|
@ -22,12 +22,17 @@ We're mostly using oslo_utils for time calculations but we're encapsulating it
|
||||
to ease maintenance in case of library modifications.
|
||||
"""
|
||||
import calendar
|
||||
import contextlib
|
||||
import datetime
|
||||
import shutil
|
||||
import six
|
||||
import sys
|
||||
import tempfile
|
||||
import yaml
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
|
||||
from oslo_utils import timeutils
|
||||
from six import moves
|
||||
from stevedore import extension
|
||||
@ -257,3 +262,16 @@ def get_metrics_conf(conf_path):
|
||||
LOG.error(exc)
|
||||
|
||||
return res
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def tempdir(**kwargs):
|
||||
tmpdir = tempfile.mkdtemp(**kwargs)
|
||||
try:
|
||||
yield tmpdir
|
||||
finally:
|
||||
try:
|
||||
shutil.rmtree(tmpdir)
|
||||
except OSError as e:
|
||||
LOG.debug('Could not remove tmpdir: %s',
|
||||
six.text_type(e))
|
||||
|
@ -132,7 +132,11 @@ function configure_cloudkitty {
|
||||
|
||||
touch $CLOUDKITTY_CONF
|
||||
|
||||
cp $CLOUDKITTY_DIR$CLOUDKITTY_CONF_DIR/policy.json $CLOUDKITTY_CONF_DIR
|
||||
# generate policy sample file
|
||||
oslopolicy-sample-generator --config-file $CLOUDKITTY_DIR/etc/oslo-policy-generator/cloudkitty.conf --output-file $CLOUDKITTY_DIR/etc/cloudkitty/policy.yaml.sample
|
||||
cp $CLOUDKITTY_DIR/etc/cloudkitty/policy.yaml.sample "$CLOUDKITTY_CONF_DIR/policy.yaml"
|
||||
iniset $CLOUDKITTY_CONF oslo_policy policy_file 'policy.yaml'
|
||||
|
||||
cp $CLOUDKITTY_DIR$CLOUDKITTY_CONF_DIR/api_paste.ini $CLOUDKITTY_CONF_DIR
|
||||
cp $CLOUDKITTY_DIR$CLOUDKITTY_CONF_DIR/metrics.yml $CLOUDKITTY_CONF_DIR
|
||||
iniset_rpc_backend cloudkitty $CLOUDKITTY_CONF DEFAULT
|
||||
|
@ -57,6 +57,8 @@ extensions = ['sphinx.ext.coverage',
|
||||
'sphinxcontrib.pecanwsme.rest',
|
||||
'sphinxcontrib.httpdomain',
|
||||
'openstackdocstheme',
|
||||
'oslo_policy.sphinxext',
|
||||
'oslo_policy.sphinxpolicygen',
|
||||
]
|
||||
|
||||
# openstackdocstheme options
|
||||
@ -65,6 +67,9 @@ bug_project = 'cloudkitty'
|
||||
bug_tag = ''
|
||||
html_last_updated_fmt = '%Y-%m-%d %H:%M'
|
||||
|
||||
policy_generator_config_file = '../../etc/oslo-policy-generator/cloudkitty.conf'
|
||||
sample_policy_basename = '_static/cloudkitty'
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
# templates_path = []
|
||||
|
||||
|
12
doc/source/configuration/policy.rst
Normal file
12
doc/source/configuration/policy.rst
Normal file
@ -0,0 +1,12 @@
|
||||
====================
|
||||
Policy configuration
|
||||
====================
|
||||
|
||||
Configuration
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
The following is an overview of all available policies in Cloudkitty. For a sample
|
||||
configuration file, refer to :doc:`samples/policy-yaml`.
|
||||
|
||||
.. show-policy::
|
||||
:config-file: ../../etc/oslo-policy-generator/cloudkitty.conf
|
11
doc/source/configuration/samples/index.rst
Normal file
11
doc/source/configuration/samples/index.rst
Normal file
@ -0,0 +1,11 @@
|
||||
==========================
|
||||
Sample configuration files
|
||||
==========================
|
||||
|
||||
Configuration files can alter how cloudkitty behaves at runtime and by default
|
||||
are located in ``/etc/cloudkitty/``. Links to sample configuration files can be
|
||||
found below:
|
||||
|
||||
.. toctree::
|
||||
|
||||
policy-yaml.rst
|
8
doc/source/configuration/samples/policy-yaml.rst
Normal file
8
doc/source/configuration/samples/policy-yaml.rst
Normal file
@ -0,0 +1,8 @@
|
||||
===========
|
||||
policy.yaml
|
||||
===========
|
||||
|
||||
Use the ``policy.yaml`` file to define additional access controls that apply to
|
||||
the Rating service:
|
||||
|
||||
.. literalinclude:: ../../_static/policy.yaml.sample
|
15
doc/source/sample_policy.rst
Normal file
15
doc/source/sample_policy.rst
Normal file
@ -0,0 +1,15 @@
|
||||
========================
|
||||
Cloudkitty Sample Policy
|
||||
========================
|
||||
|
||||
The following is a sample Cloudkitty policy file that has been auto-generated
|
||||
from default policy values in code. If you're using the default policies, then
|
||||
the maintenance of this file is not necessary, and it should not be copied into
|
||||
a deployment. Doing so will result in duplicate policy definitions. It is here
|
||||
to help explain which policy operations protect specific Cloudkitty APIs, but it
|
||||
is not suggested to copy and paste into a deployment unless you're planning on
|
||||
providing a different policy for an operation that is not the default.
|
||||
|
||||
The sample policy file can also be viewed in `file form <_static/policy.yaml.sample>`_.
|
||||
|
||||
.. literalinclude:: _static/policy.yaml.sample
|
@ -1,28 +0,0 @@
|
||||
{
|
||||
"context_is_admin": "role:admin",
|
||||
"admin_or_owner": "is_admin:True or tenant:%(tenant_id)s",
|
||||
"default": "",
|
||||
|
||||
"info:list_services_info": "",
|
||||
"info:get_service_info": "",
|
||||
"info:get_config":"",
|
||||
|
||||
"rating:list_modules": "role:admin",
|
||||
"rating:get_module": "role:admin",
|
||||
"rating:update_module": "role:admin",
|
||||
"rating:quote": "",
|
||||
|
||||
"report:list_tenants": "role:admin",
|
||||
"report:get_summary": "rule:admin_or_owner",
|
||||
"report:get_total": "rule:admin_or_owner",
|
||||
|
||||
"collector:list_mappings": "role:admin",
|
||||
"collector:get_mapping": "role:admin",
|
||||
"collector:manage_mapping": "role:admin",
|
||||
"collector:get_state": "role:admin",
|
||||
"collector:update_state": "role:admin",
|
||||
|
||||
"storage:list_data_frames": "",
|
||||
|
||||
"rating:module_config": "role:admin"
|
||||
}
|
3
etc/oslo-policy-generator/cloudkitty.conf
Normal file
3
etc/oslo-policy-generator/cloudkitty.conf
Normal file
@ -0,0 +1,3 @@
|
||||
[DEFAULT]
|
||||
output_file = etc/cloudkitty/policy.yaml.sample
|
||||
namespace = cloudkitty
|
@ -32,6 +32,12 @@ console_scripts =
|
||||
wsgi_scripts =
|
||||
cloudkitty-api = cloudkitty.api.app:build_wsgi_app
|
||||
|
||||
oslo.policy.enforcer =
|
||||
cloudkitty = cloudkitty.common.policy:get_enforcer
|
||||
|
||||
oslo.policy.policies =
|
||||
cloudkitty = cloudkitty.common.policies:list_rules
|
||||
|
||||
oslo.config.opts =
|
||||
cloudkitty.common.config = cloudkitty.common.config:list_opts
|
||||
|
||||
|
3
tox.ini
3
tox.ini
@ -34,6 +34,9 @@ commands =
|
||||
commands =
|
||||
oslo-config-generator --config-file etc/oslo-config-generator/cloudkitty.conf
|
||||
|
||||
[testenv:genpolicy]
|
||||
commands = oslopolicy-sample-generator --config-file=etc/oslo-policy-generator/cloudkitty.conf
|
||||
|
||||
[testenv:docs]
|
||||
commands = python setup.py build_sphinx
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user