Add Neutron context module and some policy methods

Here we add the context module from Neutron, although it is
currently marked as private because it will undergo changes
related to the ongoing enginefacade and Keystone V3 work.

We also add some of the roles management methods from
Neutron's policy module. These are required for the initial
DB patches that follow this one.

Partially implements: Blueprint neutron-lib

Change-Id: I77f8a05d6e3167f3096ad637d124e47ac39a83df
This commit is contained in:
Boden R
2016-04-06 15:30:53 -04:00
committed by Henry Gessau
parent 48ae867d81
commit 0c29ef7ffe
11 changed files with 545 additions and 0 deletions

142
neutron_lib/_context.py Normal file
View File

@@ -0,0 +1,142 @@
# 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.
"""Context: context for security/db session."""
import copy
import datetime
from oslo_context import context as oslo_context
from oslo_db.sqlalchemy import enginefacade
# TODO(HenryG): replace db/_api.py with the real db/api.py
from neutron_lib.db import _api as db_api
from neutron_lib import policy
class ContextBase(oslo_context.RequestContext):
"""Security context and request information.
Represents the user taking a given action within the system.
"""
def __init__(self, user_id, tenant_id, is_admin=None, roles=None,
timestamp=None, request_id=None, tenant_name=None,
user_name=None, overwrite=True, auth_token=None,
is_advsvc=None, **kwargs):
"""Object initialization.
:param overwrite: Set to False to ensure that the greenthread local
copy of the index is not overwritten.
:param kwargs: Extra arguments that might be present, but we ignore
because they possibly came in from older rpc messages.
"""
super(ContextBase, self).__init__(auth_token=auth_token,
user=user_id, tenant=tenant_id,
is_admin=is_admin,
request_id=request_id,
overwrite=overwrite,
roles=roles)
self.user_name = user_name
self.tenant_name = tenant_name
if not timestamp:
timestamp = datetime.datetime.utcnow()
self.timestamp = timestamp
self.is_advsvc = is_advsvc
if self.is_advsvc is None:
self.is_advsvc = self.is_admin or policy.check_is_advsvc(self)
if self.is_admin is None:
self.is_admin = policy.check_is_admin(self)
@property
def project_id(self):
return self.tenant
@property
def tenant_id(self):
return self.tenant
@tenant_id.setter
def tenant_id(self, tenant_id):
self.tenant = tenant_id
@property
def user_id(self):
return self.user
@user_id.setter
def user_id(self, user_id):
self.user = user_id
def to_dict(self):
context = super(ContextBase, self).to_dict()
context.update({
'user_id': self.user_id,
'tenant_id': self.tenant_id,
'project_id': self.project_id,
'timestamp': str(self.timestamp),
'tenant_name': self.tenant_name,
'project_name': self.tenant_name,
'user_name': self.user_name,
})
return context
@classmethod
def from_dict(cls, values):
return cls(**values)
def elevated(self):
"""Return a version of this context with admin flag set."""
context = copy.copy(self)
context.is_admin = True
if 'admin' not in [x.lower() for x in context.roles]:
context.roles = context.roles + ["admin"]
return context
@enginefacade.transaction_context_provider
class ContextBaseWithSession(ContextBase):
pass
class Context(ContextBaseWithSession):
def __init__(self, *args, **kwargs):
super(Context, self).__init__(*args, **kwargs)
self._session = None
@property
def session(self):
# TODO(akamyshnikova): checking for session attribute won't be needed
# when reader and writer will be used
if hasattr(super(Context, self), 'session'):
return super(Context, self).session
if self._session is None:
self._session = db_api.get_session()
return self._session
def get_admin_context():
return Context(user_id=None,
tenant_id=None,
is_admin=True,
overwrite=False)
def get_admin_context_without_session():
return ContextBase(user_id=None,
tenant_id=None,
is_admin=True)

40
neutron_lib/db/_api.py Normal file
View File

@@ -0,0 +1,40 @@
# 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.
"""
TEMPORARY: use the old EngineFacade and lazy init.
TODO(HenryG): replace this file with the new db/api.py from neutron.
"""
from oslo_config import cfg
from oslo_db.sqlalchemy import session
_FACADE = None
def _create_facade_lazily():
global _FACADE
# NOTE: This is going to change with bug 1520719
if _FACADE is None:
_FACADE = session.EngineFacade.from_config(cfg.CONF, sqlite_fk=True)
return _FACADE
def get_session(autocommit=True, expire_on_commit=False, use_slave=False):
"""Helper method to grab session."""
facade = _create_facade_lazily()
return facade.get_session(autocommit=autocommit,
expire_on_commit=expire_on_commit,
use_slave=use_slave)

View File

@@ -240,3 +240,11 @@ class NetworkTunnelRangeError(NeutronException):
if isinstance(kwargs['tunnel_range'], tuple):
kwargs['tunnel_range'] = "%d:%d" % kwargs['tunnel_range']
super(NetworkTunnelRangeError, self).__init__(**kwargs)
class PolicyInitError(NeutronException):
message = _("Failed to initialize policy %(policy)s because %(reason)s.")
class PolicyCheckError(NeutronException):
message = _("Failed to check policy %(policy)s because %(reason)s.")

61
neutron_lib/policy.py Normal file
View File

@@ -0,0 +1,61 @@
# 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_config import cfg
from oslo_policy import policy
_ENFORCER = None
_ADMIN_CTX_POLICY = 'context_is_admin'
_ADVSVC_CTX_POLICY = 'context_is_advsvc'
def reset():
global _ENFORCER
if _ENFORCER:
_ENFORCER.clear()
_ENFORCER = None
def init(conf=cfg.CONF, policy_file=None):
"""Init an instance of the Enforcer class."""
global _ENFORCER
if not _ENFORCER:
_ENFORCER = policy.Enforcer(conf, policy_file=policy_file)
_ENFORCER.load_rules(True)
def refresh(policy_file=None):
"""Reset policy and init a new instance of Enforcer."""
reset()
init(policy_file=policy_file)
def check_is_admin(context):
"""Verify context has admin rights according to policy settings."""
init()
# the target is user-self
credentials = context.to_dict()
if _ADMIN_CTX_POLICY not in _ENFORCER.rules:
return False
return _ENFORCER.enforce(_ADMIN_CTX_POLICY, credentials, credentials)
def check_is_advsvc(context):
"""Verify context has advsvc rights according to policy settings."""
init()
# the target is user-self
credentials = context.to_dict()
if _ADVSVC_CTX_POLICY not in _ENFORCER.rules:
return False
return _ENFORCER.enforce(_ADVSVC_CTX_POLICY, credentials, credentials)

View File

@@ -23,11 +23,13 @@ import mock
from oslo_config import cfg
from oslo_db import options as db_options
from oslo_utils import strutils
import pbr.version
import six
import testtools
from neutron_lib._i18n import _
from neutron_lib import constants
from neutron_lib.tests import _post_mortem_debug as post_mortem_debug
from neutron_lib.tests import _tools as tools
@@ -99,6 +101,19 @@ class AttributeDict(dict):
class BaseTestCase(testtools.TestCase):
@staticmethod
def config_parse(conf=None, args=None):
"""Create the default configurations."""
if args is None:
args = []
args += ['--config-file', etcdir('neutron_lib.conf')]
if conf is None:
version_info = pbr.version.VersionInfo('neutron-lib')
cfg.CONF(args=args, project='neutron_lib',
version='%%(prog)s %s' % version_info.release_string())
else:
conf(args)
def setUp(self):
super(BaseTestCase, self).setUp()
@@ -110,6 +125,12 @@ class BaseTestCase(testtools.TestCase):
sqlite_db='', max_pool_size=10,
max_overflow=20, pool_timeout=10)
self.useFixture(fixtures.MonkeyPatch(
'oslo_config.cfg.find_config_files',
lambda project=None, prog=None, extension=None: []))
self.setup_config()
# Configure this first to ensure pm debugging support for setUp()
debugger = os.environ.get('OS_POST_MORTEM_DEBUGGER')
if debugger:
@@ -195,3 +216,7 @@ class BaseTestCase(testtools.TestCase):
self.assertEqual(v, actual_superset[k],
"Key %(key)s expected: %(exp)r, actual %(act)r" %
{'key': k, 'exp': v, 'act': actual_superset[k]})
def setup_config(self, args=None):
"""Tests that need a non-default config can override this method."""
self.config_parse(args=args)

View File

@@ -0,0 +1,8 @@
[DEFAULT]
# Show debugging output in logs (sets DEBUG log level output)
debug = False
lock_path = $state_path/lock
[database]
connection = 'sqlite://'

View File

@@ -0,0 +1,2 @@
{
}

View File

@@ -0,0 +1,5 @@
{
"context_is_admin": "role:admin",
"context_is_advsvc": "role:advsvc",
"default": "rule:admin_or_owner"
}

View File

@@ -0,0 +1,183 @@
# 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 mock
from oslo_context import context as oslo_context
from testtools import matchers
from neutron_lib import _context
from neutron_lib.tests import _base
class TestNeutronContext(_base.BaseTestCase):
def test_neutron_context_create(self):
ctx = _context.Context('user_id', 'tenant_id')
self.assertEqual('user_id', ctx.user_id)
self.assertEqual('tenant_id', ctx.project_id)
self.assertEqual('tenant_id', ctx.tenant_id)
request_id = ctx.request_id
if isinstance(request_id, bytes):
request_id = request_id.decode('utf-8')
self.assertThat(request_id, matchers.StartsWith('req-'))
self.assertEqual('user_id', ctx.user)
self.assertEqual('tenant_id', ctx.tenant)
self.assertIsNone(ctx.user_name)
self.assertIsNone(ctx.tenant_name)
self.assertIsNone(ctx.auth_token)
def test_neutron_context_getter_setter(self):
ctx = _context.Context('Anakin', 'Skywalker')
self.assertEqual('Anakin', ctx.user_id)
self.assertEqual('Skywalker', ctx.tenant_id)
ctx.user_id = 'Darth'
ctx.tenant_id = 'Vader'
self.assertEqual('Darth', ctx.user_id)
self.assertEqual('Vader', ctx.tenant_id)
def test_neutron_context_create_with_name(self):
ctx = _context.Context('user_id', 'tenant_id',
tenant_name='tenant_name',
user_name='user_name')
# Check name is set
self.assertEqual('user_name', ctx.user_name)
self.assertEqual('tenant_name', ctx.tenant_name)
# Check user/tenant contains its ID even if user/tenant_name is passed
self.assertEqual('user_id', ctx.user)
self.assertEqual('tenant_id', ctx.tenant)
def test_neutron_context_create_with_request_id(self):
ctx = _context.Context('user_id', 'tenant_id', request_id='req_id_xxx')
self.assertEqual('req_id_xxx', ctx.request_id)
def test_neutron_context_create_with_timestamp(self):
now = "Right Now!"
ctx = _context.Context('user_id', 'tenant_id', timestamp=now)
self.assertEqual(now, ctx.timestamp)
def test_neutron_context_create_is_advsvc(self):
ctx = _context.Context('user_id', 'tenant_id', is_advsvc=True)
self.assertFalse(ctx.is_admin)
self.assertTrue(ctx.is_advsvc)
def test_neutron_context_create_with_auth_token(self):
ctx = _context.Context('user_id', 'tenant_id',
auth_token='auth_token_xxx')
self.assertEqual('auth_token_xxx', ctx.auth_token)
def test_neutron_context_from_dict(self):
owner = {'user_id': 'Luke', 'tenant_id': 'Skywalker'}
ctx = _context.Context.from_dict(owner)
self.assertEqual(owner['user_id'], ctx.user_id)
self.assertEqual(owner['tenant_id'], ctx.tenant_id)
def test_neutron_context_to_dict(self):
ctx = _context.Context('user_id', 'tenant_id')
ctx_dict = ctx.to_dict()
self.assertEqual('user_id', ctx_dict['user_id'])
self.assertEqual('tenant_id', ctx_dict['project_id'])
self.assertEqual(ctx.request_id, ctx_dict['request_id'])
self.assertEqual('user_id', ctx_dict['user'])
self.assertEqual('tenant_id', ctx_dict['tenant'])
self.assertIsNone(ctx_dict['user_name'])
self.assertIsNone(ctx_dict['tenant_name'])
self.assertIsNone(ctx_dict['project_name'])
self.assertIsNone(ctx_dict['auth_token'])
def test_neutron_context_to_dict_with_name(self):
ctx = _context.Context('user_id', 'tenant_id',
tenant_name='tenant_name',
user_name='user_name')
ctx_dict = ctx.to_dict()
self.assertEqual('user_name', ctx_dict['user_name'])
self.assertEqual('tenant_name', ctx_dict['tenant_name'])
self.assertEqual('tenant_name', ctx_dict['project_name'])
def test_neutron_context_to_dict_with_auth_token(self):
ctx = _context.Context('user_id', 'tenant_id',
auth_token='auth_token_xxx')
ctx_dict = ctx.to_dict()
self.assertEqual('auth_token_xxx', ctx_dict['auth_token'])
def test_neutron_context_admin_to_dict(self):
ctx = _context.get_admin_context()
ctx_dict = ctx.to_dict()
self.assertIsNone(ctx_dict['user_id'])
self.assertIsNone(ctx_dict['tenant_id'])
self.assertIsNone(ctx_dict['auth_token'])
self.assertTrue(ctx_dict['is_admin'])
self.assertIsNotNone(ctx.session)
self.assertNotIn('session', ctx_dict)
def test_neutron_context_admin_without_session_to_dict(self):
ctx = _context.get_admin_context_without_session()
ctx_dict = ctx.to_dict()
self.assertIsNone(ctx_dict['user_id'])
self.assertIsNone(ctx_dict['tenant_id'])
self.assertIsNone(ctx_dict['auth_token'])
self.assertFalse(hasattr(ctx, 'session'))
def test_neutron_context_elevated_retains_request_id(self):
ctx = _context.Context('user_id', 'tenant_id')
self.assertFalse(ctx.is_admin)
req_id_before = ctx.request_id
elevated_ctx = ctx.elevated()
self.assertTrue(elevated_ctx.is_admin)
self.assertEqual(req_id_before, elevated_ctx.request_id)
def test_neutron_context_elevated_idempotent(self):
ctx = _context.Context('user_id', 'tenant_id')
self.assertFalse(ctx.is_admin)
elevated_ctx = ctx.elevated()
self.assertTrue(elevated_ctx.is_admin)
elevated2_ctx = elevated_ctx.elevated()
self.assertTrue(elevated2_ctx.is_admin)
def test_neutron_context_overwrite(self):
ctx1 = _context.Context('user_id', 'tenant_id')
self.assertEqual(ctx1.request_id,
oslo_context.get_current().request_id)
# If overwrite is not specified, request_id should be updated.
ctx2 = _context.Context('user_id', 'tenant_id')
self.assertNotEqual(ctx2.request_id, ctx1.request_id)
self.assertEqual(ctx2.request_id,
oslo_context.get_current().request_id)
# If overwrite is specified, request_id should be kept.
ctx3 = _context.Context('user_id', 'tenant_id', overwrite=False)
self.assertNotEqual(ctx3.request_id, ctx2.request_id)
self.assertEqual(ctx2.request_id,
oslo_context.get_current().request_id)
def test_neutron_context_get_admin_context_not_update_local_store(self):
ctx = _context.Context('user_id', 'tenant_id')
req_id_before = oslo_context.get_current().request_id
self.assertEqual(ctx.request_id, req_id_before)
ctx_admin = _context.get_admin_context()
self.assertEqual(req_id_before, oslo_context.get_current().request_id)
self.assertNotEqual(req_id_before, ctx_admin.request_id)
@mock.patch.object(_context.ContextBaseWithSession, 'session')
def test_superclass_session(self, mocked_session):
ctx = _context.Context('user_id', 'tenant_id')
# make sure context uses parent class session that is mocked
self.assertEqual(mocked_session, ctx.session)
def test_session_cached(self):
ctx = _context.Context('user_id', 'tenant_id')
session1 = ctx.session
session2 = ctx.session
self.assertIs(session1, session2)

View File

@@ -0,0 +1,68 @@
# 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 mock
from neutron_lib import _context
from neutron_lib import policy
from neutron_lib.tests import _base as base
class TestPolicyEnforcer(base.BaseTestCase):
def setUp(self):
super(TestPolicyEnforcer, self).setUp()
# Isolate one _ENFORCER per test case
mock.patch.object(policy, '_ENFORCER', None).start()
def test_init_reset_refresh(self):
self.assertIsNone(policy._ENFORCER)
policy.init()
self.assertIsNotNone(policy._ENFORCER)
policy.reset()
self.assertIsNone(policy._ENFORCER)
policy.refresh()
self.assertIsNotNone(policy._ENFORCER)
def test_check_user_is_not_admin(self):
ctx = _context.Context('me', 'my_project')
self.assertFalse(policy.check_is_admin(ctx))
def test_check_user_elevated_is_admin(self):
ctx = _context.Context('me', 'my_project', roles=['user']).elevated()
self.assertTrue(policy.check_is_admin(ctx))
def test_check_is_admin_no_roles_no_admin(self):
policy.init(policy_file='no_policy.json')
ctx = _context.Context('me', 'my_project', roles=['user']).elevated()
# With no admin role, elevated() should not work.
self.assertFalse(policy.check_is_admin(ctx))
def test_check_is_advsvc_role(self):
ctx = _context.Context('me', 'my_project', roles=['advsvc'])
self.assertTrue(policy.check_is_advsvc(ctx))
def test_check_is_not_advsvc_user(self):
ctx = _context.Context('me', 'my_project', roles=['user'])
self.assertFalse(policy.check_is_advsvc(ctx))
def test_check_is_not_advsvc_admin(self):
ctx = _context.Context('me', 'my_project').elevated()
self.assertTrue(policy.check_is_admin(ctx))
self.assertFalse(policy.check_is_advsvc(ctx))
def test_check_is_advsvc_no_roles_no_advsvc(self):
policy.init(policy_file='no_policy.json')
ctx = _context.Context('me', 'my_project', roles=['advsvc'])
# No advsvc role in the policy file, so cannot assume the role.
self.assertFalse(policy.check_is_advsvc(ctx))

View File

@@ -7,8 +7,11 @@ Babel>=2.3.4 # BSD
debtcollector>=1.2.0 # Apache-2.0
oslo.config>=3.12.0 # Apache-2.0
oslo.context>=2.4.0,!=2.6.0 # Apache-2.0
oslo.db>=4.1.0 # Apache-2.0
oslo.i18n>=2.1.0 # Apache-2.0
oslo.log>=1.14.0 # Apache-2.0
oslo.messaging>=5.2.0 # Apache-2.0
oslo.policy>=1.9.0 # Apache-2.0
oslo.service>=1.10.0 # Apache-2.0
oslo.utils>=3.16.0 # Apache-2.0