Policy in code

Adding the beginning implementation for registering and using
default policy rules in code. Rules are defined in the new
policies module and added to the return list __init__.py.
Default policies can now be maintained in code and registered
via listing mechanisms in the policies module. As we go, we
can remove the duplicated default policies from our policy.json
file.

This commit specifically:
- Creates a new module called `policies` to hold our in code defaults.
- Ensure we pass our in code policy list to our policy ENFORCER.
- Add base policy module for common policy rules.
- Add service default policy module for policy rules.
- Add endpoint default policy module for policy rules.
- Add regions default policy module for policy rules.

partially-implements blueprint policy-in-code
Co-Authored-By: Richard Avelar csravelar@gmail.com
Change-Id: Ic47b1e8b0d479032d8a7b9891ed9800be7036d94
This commit is contained in:
Anthony Washington 2017-02-17 21:07:54 +00:00
parent c16f68e3a3
commit c734b58581
13 changed files with 314 additions and 117 deletions

View File

@ -1,33 +1,4 @@
{
"admin_required": "role:admin or is_admin:1",
"service_role": "role:service",
"service_or_admin": "rule:admin_required or rule:service_role",
"owner" : "user_id:%(user_id)s",
"admin_or_owner": "rule:admin_required or rule:owner",
"token_subject": "user_id:%(target.token.user_id)s",
"admin_or_token_subject": "rule:admin_required or rule:token_subject",
"service_admin_or_token_subject": "rule:service_or_admin or rule:token_subject",
"default": "rule:admin_required",
"identity:get_region": "",
"identity:list_regions": "",
"identity:create_region": "rule:admin_required",
"identity:update_region": "rule:admin_required",
"identity:delete_region": "rule:admin_required",
"identity:get_service": "rule:admin_required",
"identity:list_services": "rule:admin_required",
"identity:create_service": "rule:admin_required",
"identity:update_service": "rule:admin_required",
"identity:delete_service": "rule:admin_required",
"identity:get_endpoint": "rule:admin_required",
"identity:list_endpoints": "rule:admin_required",
"identity:create_endpoint": "rule:admin_required",
"identity:update_endpoint": "rule:admin_required",
"identity:delete_endpoint": "rule:admin_required",
"identity:get_domain": "rule:admin_required or token.project.domain.id:%(target.domain.id)s",
"identity:list_domains": "rule:admin_required",
"identity:create_domain": "rule:admin_required",

View File

@ -23,6 +23,7 @@ import six
from keystone.common import authorization
from keystone.common import dependency
from keystone.common import driver_hints
from keystone.common import policy
from keystone.common import utils
from keystone.common import wsgi
import keystone.conf
@ -157,9 +158,7 @@ def protected(callback=None):
# Add in the kwargs, which means that any entity provided as a
# parameter for calls like create and update will be included.
policy_dict.update(kwargs)
self.policy_api.enforce(creds,
action,
utils.flatten_dict(policy_dict))
policy.enforce(creds, action, utils.flatten_dict(policy_dict))
LOG.debug('RBAC: Authorization granted')
return f(self, request, *args, **kwargs)
return inner
@ -225,9 +224,7 @@ def filterprotected(*filters, **callback):
for key in kwargs:
target[key] = kwargs[key]
self.policy_api.enforce(creds,
action,
utils.flatten_dict(target))
policy.enforce(creds, action, utils.flatten_dict(target))
LOG.debug('RBAC: Authorization granted')
else:
@ -772,9 +769,7 @@ class V3Controller(wsgi.Application):
policy_dict.update(prep_info['input_attr'])
if 'filter_attr' in prep_info:
policy_dict.update(prep_info['filter_attr'])
self.policy_api.enforce(creds,
action,
utils.flatten_dict(policy_dict))
policy.enforce(creds, action, utils.flatten_dict(policy_dict))
LOG.debug('RBAC: Authorization granted')
@classmethod

View File

@ -0,0 +1,27 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import itertools
from keystone.common.policies import base
from keystone.common.policies import endpoint
from keystone.common.policies import region
from keystone.common.policies import service
def list_rules():
return itertools.chain(
base.list_rules(),
endpoint.list_rules(),
region.list_rules(),
service.list_rules()
)

View File

@ -0,0 +1,52 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from oslo_policy import policy
IDENTITY = 'identity:%s'
RULE_ADMIN_REQUIRED = 'rule:admin_required'
RULE_ADMIN_OR_OWNER = 'rule:admin_or_owner'
rules = [
policy.RuleDefault(
name='admin_required',
check_str='role:admin or is_admin:1'),
policy.RuleDefault(
name='service_role',
check_str='role:service'),
policy.RuleDefault(
name='service_or_admin',
check_str='rule:admin_required or rule:service_role'),
policy.RuleDefault(
name='owner',
check_str='user_id:%(user_id)s'),
policy.RuleDefault(
name='admin_or_owner',
check_str='rule:admin_required or rule:owner'),
policy.RuleDefault(
name='token_subject',
check_str='user_id:%(target.token.user_id)s'),
policy.RuleDefault(
name='admin_or_token_subject',
check_str='rule:admin_required or rule:token_subject'),
policy.RuleDefault(
name='service_admin_or_token_subject',
check_str='rule:service_or_admin or rule:token_subject'),
policy.RuleDefault(
name='default',
check_str='rule:admin_required')
]
def list_rules():
return rules

View File

@ -0,0 +1,37 @@
# 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 keystone.common.policies import base
endpoint_policies = [
policy.RuleDefault(
name=base.IDENTITY % 'get_endpoint',
check_str=base.RULE_ADMIN_REQUIRED),
policy.RuleDefault(
name=base.IDENTITY % 'list_endpoints',
check_str=base.RULE_ADMIN_REQUIRED),
policy.RuleDefault(
name=base.IDENTITY % 'create_endpoint',
check_str=base.RULE_ADMIN_REQUIRED),
policy.RuleDefault(
name=base.IDENTITY % 'update_endpoint',
check_str=base.RULE_ADMIN_REQUIRED),
policy.RuleDefault(
name=base.IDENTITY % 'delete_endpoint',
check_str=base.RULE_ADMIN_REQUIRED)
]
def list_rules():
return endpoint_policies

View File

@ -0,0 +1,37 @@
# 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 keystone.common.policies import base
region_policies = [
policy.RuleDefault(
name=base.IDENTITY % 'get_region',
check_str=''),
policy.RuleDefault(
name=base.IDENTITY % 'list_regions',
check_str=''),
policy.RuleDefault(
name=base.IDENTITY % 'create_region',
check_str=base.RULE_ADMIN_REQUIRED),
policy.RuleDefault(
name=base.IDENTITY % 'update_region',
check_str=base.RULE_ADMIN_REQUIRED),
policy.RuleDefault(
name=base.IDENTITY % 'delete_region',
check_str=base.RULE_ADMIN_REQUIRED),
]
def list_rules():
return region_policies

View File

@ -0,0 +1,37 @@
# 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 keystone.common.policies import base
service_policies = [
policy.RuleDefault(
name=base.IDENTITY % 'get_service',
check_str=base.RULE_ADMIN_REQUIRED),
policy.RuleDefault(
name=base.IDENTITY % 'list_services',
check_str=base.RULE_ADMIN_REQUIRED),
policy.RuleDefault(
name=base.IDENTITY % 'create_service',
check_str=base.RULE_ADMIN_REQUIRED),
policy.RuleDefault(
name=base.IDENTITY % 'update_service',
check_str=base.RULE_ADMIN_REQUIRED),
policy.RuleDefault(
name=base.IDENTITY % 'delete_service',
check_str=base.RULE_ADMIN_REQUIRED)
]
def list_rules():
return service_policies

67
keystone/common/policy.py Normal file
View File

@ -0,0 +1,67 @@
# 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 as common_policy
from keystone.common import policies
import keystone.conf
from keystone import exception
CONF = keystone.conf.CONF
_ENFORCER = None
def reset():
global _ENFORCER
_ENFORCER = None
def init():
global _ENFORCER
if not _ENFORCER:
_ENFORCER = common_policy.Enforcer(CONF)
register_rules(_ENFORCER)
def enforce(credentials, action, target, do_raise=True):
"""Verify that the action is valid on the target in this context.
:param credentials: user credentials
:param action: string representing the action to be checked, which should
be colon separated for clarity.
: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':
object.project_id}
:raises keystone.exception.Forbidden: If verification fails.
Actions should be colon separated for clarity. For example:
* identity:list_users
"""
init()
# Add the exception arguments if asked to do a raise
extra = {}
if do_raise:
extra.update(exc=exception.ForbiddenAction, action=action,
do_raise=do_raise)
return _ENFORCER.enforce(action, target, credentials, **extra)
def register_rules(enforcer):
enforcer.register_defaults(policies.list_rules())

View File

@ -37,6 +37,7 @@ import webob.exc
from keystone.common import dependency
from keystone.common import json_home
from keystone.common import policy
from keystone.common import request as request_mod
from keystone.common import utils
import keystone.conf
@ -312,7 +313,7 @@ class Application(BaseApplication):
creds['roles'] = user_token_ref.role_names
# Accept either is_admin or the admin role
self.policy_api.enforce(creds, 'admin_required', {})
policy.enforce(creds, 'admin_required', {})
def _attribute_is_empty(self, ref, attribute):
"""Determine if the attribute in ref is empty or None."""

View File

@ -16,8 +16,8 @@
"""Policy engine for keystone."""
from oslo_log import log
from oslo_policy import policy as common_policy
from keystone.common import policy
import keystone.conf
from keystone import exception
from keystone.policy.backends import base
@ -27,55 +27,13 @@ CONF = keystone.conf.CONF
LOG = log.getLogger(__name__)
_ENFORCER = None
def reset():
global _ENFORCER
_ENFORCER = None
def init():
global _ENFORCER
if not _ENFORCER:
_ENFORCER = common_policy.Enforcer(CONF)
def enforce(credentials, action, target, do_raise=True):
"""Verify that the action is valid on the target in this context.
:param credentials: user credentials
:param action: string representing the action to be checked, which should
be colon separated for clarity.
: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':
object.project_id}
:raises keystone.exception.Forbidden: If verification fails.
Actions should be colon separated for clarity. For example:
* identity:list_users
"""
init()
# Add the exception arguments if asked to do a raise
extra = {}
if do_raise:
extra.update(exc=exception.ForbiddenAction, action=action,
do_raise=do_raise)
return _ENFORCER.enforce(action, target, credentials, **extra)
class Policy(base.PolicyDriverBase):
def enforce(self, credentials, action, target):
msg = 'enforce %(action)s: %(credentials)s'
LOG.debug(msg, {
'action': action,
'credentials': credentials})
enforce(credentials, action, target)
policy.enforce(credentials, action, target)
def create_policy(self, policy_id, policy):
raise exception.NotImplemented()

View File

@ -14,7 +14,7 @@
import fixtures
from oslo_policy import opts
from keystone.policy.backends import rules
from keystone.common import policy
class Policy(fixtures.Fixture):
@ -29,5 +29,5 @@ class Policy(fixtures.Fixture):
opts.set_defaults(self._config_fixture.conf)
self._config_fixture.config(group='oslo_policy',
policy_file=self._policy_file)
rules.init()
self.addCleanup(rules.reset)
policy.init()
self.addCleanup(policy.reset)

View File

@ -21,9 +21,10 @@ from oslo_policy import policy as common_policy
import six
from testtools import matchers
from keystone.common import policies
from keystone.common import policy
import keystone.conf
from keystone import exception
from keystone.policy.backends import rules
from keystone.tests import unit
from keystone.tests.unit import ksfixtures
from keystone.tests.unit.ksfixtures import temporaryfile
@ -50,11 +51,11 @@ class PolicyFileTestCase(unit.TestCase):
empty_credentials = {}
with open(self.tmpfilename, "w") as policyfile:
policyfile.write("""{"example:test": []}""")
rules.enforce(empty_credentials, action, self.target)
policy.enforce(empty_credentials, action, self.target)
with open(self.tmpfilename, "w") as policyfile:
policyfile.write("""{"example:test": ["false:false"]}""")
rules._ENFORCER.clear()
self.assertRaises(exception.ForbiddenAction, rules.enforce,
policy._ENFORCER.clear()
self.assertRaises(exception.ForbiddenAction, policy.enforce,
empty_credentials, action, self.target)
@ -81,39 +82,39 @@ class PolicyTestCase(unit.TestCase):
def _set_rules(self):
these_rules = common_policy.Rules.from_dict(self.rules)
rules._ENFORCER.set_rules(these_rules)
policy._ENFORCER.set_rules(these_rules)
def test_enforce_nonexistent_action_throws(self):
action = "example:noexist"
self.assertRaises(exception.ForbiddenAction, rules.enforce,
self.assertRaises(exception.ForbiddenAction, policy.enforce,
self.credentials, action, self.target)
def test_enforce_bad_action_throws(self):
action = "example:denied"
self.assertRaises(exception.ForbiddenAction, rules.enforce,
self.assertRaises(exception.ForbiddenAction, policy.enforce,
self.credentials, action, self.target)
def test_enforce_good_action(self):
action = "example:allowed"
rules.enforce(self.credentials, action, self.target)
policy.enforce(self.credentials, action, self.target)
def test_templatized_enforcement(self):
target_mine = {'project_id': 'fake'}
target_not_mine = {'project_id': 'another'}
credentials = {'project_id': 'fake', 'roles': []}
action = "example:my_file"
rules.enforce(credentials, action, target_mine)
self.assertRaises(exception.ForbiddenAction, rules.enforce,
policy.enforce(credentials, action, target_mine)
self.assertRaises(exception.ForbiddenAction, policy.enforce,
credentials, action, target_not_mine)
def test_early_AND_enforcement(self):
action = "example:early_and_fail"
self.assertRaises(exception.ForbiddenAction, rules.enforce,
self.assertRaises(exception.ForbiddenAction, policy.enforce,
self.credentials, action, self.target)
def test_early_OR_enforcement(self):
action = "example:early_or_success"
rules.enforce(self.credentials, action, self.target)
policy.enforce(self.credentials, action, self.target)
def test_ignore_case_role_check(self):
lowercase_action = "example:lowercase_admin"
@ -121,8 +122,8 @@ class PolicyTestCase(unit.TestCase):
# NOTE(dprince): We mix case in the Admin role here to ensure
# case is ignored
admin_credentials = {'roles': ['AdMiN']}
rules.enforce(admin_credentials, lowercase_action, self.target)
rules.enforce(admin_credentials, uppercase_action, self.target)
policy.enforce(admin_credentials, lowercase_action, self.target)
policy.enforce(admin_credentials, uppercase_action, self.target)
class DefaultPolicyTestCase(unit.TestCase):
@ -142,21 +143,21 @@ class DefaultPolicyTestCase(unit.TestCase):
# monkeypatch load_roles() so it does nothing. This seem like a bug in
# Oslo policy as we shouldn't have to reload the rules if they have
# already been set using set_rules().
self._old_load_rules = rules._ENFORCER.load_rules
self.addCleanup(setattr, rules._ENFORCER, 'load_rules',
self._old_load_rules = policy._ENFORCER.load_rules
self.addCleanup(setattr, policy._ENFORCER, 'load_rules',
self._old_load_rules)
rules._ENFORCER.load_rules = lambda *args, **kwargs: None
policy._ENFORCER.load_rules = lambda *args, **kwargs: None
def _set_rules(self, default_rule):
these_rules = common_policy.Rules.from_dict(self.rules, default_rule)
rules._ENFORCER.set_rules(these_rules)
policy._ENFORCER.set_rules(these_rules)
def test_policy_called(self):
self.assertRaises(exception.ForbiddenAction, rules.enforce,
self.assertRaises(exception.ForbiddenAction, policy.enforce,
self.credentials, "example:exist", {})
def test_not_found_policy_calls_default(self):
rules.enforce(self.credentials, "example:noexist", {})
policy.enforce(self.credentials, "example:noexist", {})
def test_default_not_found(self):
new_default_rule = "default_noexist"
@ -164,9 +165,9 @@ class DefaultPolicyTestCase(unit.TestCase):
# as it is recreating the rules with its own default_rule instead
# of the default_rule passed in from set_rules(). I think this is a
# bug in Oslo policy.
rules._ENFORCER.default_rule = new_default_rule
policy._ENFORCER.default_rule = new_default_rule
self._set_rules(new_default_rule)
self.assertRaises(exception.ForbiddenAction, rules.enforce,
self.assertRaises(exception.ForbiddenAction, policy.enforce,
self.credentials, "example:noexist", {})
@ -175,6 +176,18 @@ class PolicyJsonTestCase(unit.TestCase):
def _load_entries(self, filename):
return set(json.load(open(filename)))
def _add_missing_default_rules(self, rules):
"""Add default rules and their values to the given rules dict.
The given rules dict may have an incomplete set of policy rules.
This method will add the default policy rules and their values to the
dict. It will not override the existing rules. This method is temporary
and is only needed until we move all policy.json rules into code.
"""
for rule in policies.list_rules():
if rule.name not in rules:
rules[rule.name] = rule.check_str
def test_json_examples_have_matching_entries(self):
policy_keys = self._load_entries(unit.dirs.etc('policy.json'))
cloud_policy_keys = self._load_entries(
@ -202,9 +215,11 @@ class PolicyJsonTestCase(unit.TestCase):
'is_admin_project': True, 'project_id': None,
'domain_id': uuid.uuid4().hex}
standard_policy = unit.dirs.etc('policy.json')
enforcer = common_policy.Enforcer(CONF, policy_file=standard_policy)
result = enforcer.enforce(action, target, credentials)
# Since we are moving policy.json defaults to code, we instead call
# `policy.init()` which does the enforce setup for us with the added
# bonus of registering the in code default policies.
policy.init()
result = policy._ENFORCER.enforce(action, target, credentials)
self.assertTrue(result)
domain_policy = unit.dirs.etc('policy.v3cloudsample.json')
@ -215,8 +230,8 @@ class PolicyJsonTestCase(unit.TestCase):
def test_all_targets_documented(self):
# All the targets in the sample policy file must be documented in
# doc/source/policy_mapping.rst.
policy_keys = self._load_entries(unit.dirs.etc('policy.json'))
policy_keys = json.load(open((unit.dirs.etc('policy.json'))))
self._add_missing_default_rules(policy_keys)
# These keys are in the policy.json but aren't targets.
policy_rule_keys = [

View File

@ -33,12 +33,12 @@ from testtools import testcase
from keystone import auth
from keystone.auth.plugins import totp
from keystone.common import policy
from keystone.common import utils
import keystone.conf
from keystone.credential.providers import fernet as credential_fernet
from keystone import exception
from keystone.identity.backends import resource_options as ro
from keystone.policy.backends import rules
from keystone.tests.common import auth as common_auth
from keystone.tests import unit
from keystone.tests.unit import ksfixtures
@ -4276,7 +4276,7 @@ class TrustAPIBehavior(test_v3.RestfulTestCase):
self.chained_trust_ref['roles'] = [{'id': role['id']}]
# Bypass policy enforcement
with mock.patch.object(rules, 'enforce', return_value=True):
with mock.patch.object(policy, 'enforce', return_value=True):
self.post('/OS-TRUST/trusts',
body={'trust': self.chained_trust_ref},
token=trust_token,
@ -4969,7 +4969,7 @@ class TestTrustChain(test_v3.RestfulTestCase):
self.identity_api.update_user(disabled['id'], disabled)
# Bypass policy enforcement
with mock.patch.object(rules, 'enforce', return_value=True):
with mock.patch.object(policy, 'enforce', return_value=True):
headers = {'X-Subject-Token': self.last_token}
self.head('/auth/tokens', headers=headers,
expected_status=http_client.FORBIDDEN)
@ -4981,7 +4981,7 @@ class TestTrustChain(test_v3.RestfulTestCase):
# Bypass policy enforcement
# Delete trustee will invalidate the trust.
with mock.patch.object(rules, 'enforce', return_value=True):
with mock.patch.object(policy, 'enforce', return_value=True):
headers = {'X-Subject-Token': self.last_token}
self.head('/auth/tokens', headers=headers,
expected_status=http_client.NOT_FOUND)