Use built-in oslo context de/serialization

The oslo context library has built-in mechanisms to deserialize a
context object from a set of headers; Blazar's built in extension of the
context class was ignoring several possibly-important pieces of
information, notably the Keystone domain name.

To fix, this removes much of the custom logic in the BlazarContext and
keeps only the two important bits:

1. A stack of contexts is maintained to allow for nested operations w/
   different sets of credentials
2. The service_catalog is preserved. It's unclear if this is really
   needed long-term, but some code still relies on it. Also unclear why
   the oslo context doesn't include this when parsing headers.

Support for multiple domains is included as part of this changeset.
Before, it was assumed that all users (admins and project users) were
part of the default domain.

Closes-Bug: #1881162
Change-Id: I75fcd97cf7a53d17c909620fcf41a8b5a3699dfa
(cherry picked from commit ed238925f9)
This commit is contained in:
Jason Anderson 2020-05-27 15:41:35 -05:00 committed by Pierre Riteau
parent 8d905a2f5b
commit fc40083824
14 changed files with 153 additions and 136 deletions

View File

@ -27,18 +27,5 @@ def ctx_from_headers(headers):
except TypeError:
raise exceptions.WrongFormat()
kwargs = {"user_id": headers['X-User-Id'],
"project_id": headers['X-Project-Id'],
"auth_token": headers['X-Auth-Token'],
"service_catalog": service_catalog,
"user_name": headers['X-User-Name'],
"project_name": headers['X-Project-Name'],
"roles": list(
map(str.strip, headers['X-Roles'].split(',')))}
# For v1 only, request_id and global_request_id will be available.
if headers.environ['PATH_INFO'].startswith('/v1'):
kwargs['request_id'] = headers.environ['openstack.request_id']
kwargs['global_request_id'] = headers.environ.get(
'openstack.global_request_id')
return context.BlazarContext(**kwargs)
return context.BlazarContext.from_environ(headers.environ,
service_catalog=service_catalog)

View File

@ -15,31 +15,29 @@
import threading
from oslo_config import cfg
from oslo_context import context
CONF = cfg.CONF
class BlazarContext(context.RequestContext):
# service_catalog is not by default read from a dict
# when deserializing a context.
FROM_DICT_EXTRA_KEYS = ['service_catalog']
_context_stack = threading.local()
def __init__(self, user_id=None, project_id=None, project_name=None,
service_catalog=None, user_name=None, **kwargs):
def __init__(self, service_catalog=None, **kwargs):
# NOTE(neha-alhat): During serializing/deserializing context object
# over the RPC layer, below extra parameters which are passed by
# `oslo.messaging` are popped as these parameters are not required.
kwargs.pop('client_timeout', None)
kwargs.pop('user_identity', None)
kwargs.pop('project', None)
if user_id:
kwargs['user_id'] = user_id
if project_id:
kwargs['project_id'] = project_id
super(BlazarContext, self).__init__(**kwargs)
self.project_name = project_name
self.user_name = user_name
self.service_catalog = service_catalog or []
if self.is_admin and 'admin' not in self.roles:
@ -65,28 +63,35 @@ class BlazarContext(context.RequestContext):
except (AttributeError, IndexError):
raise RuntimeError("Context isn't available here")
# NOTE(yorik-sar): as long as oslo.rpc requires this
def to_dict(self):
result = super(BlazarContext, self).to_dict()
result['user_id'] = self.user_id
result['user_name'] = self.user_name
result['project_id'] = self.project_id
result['project_name'] = self.project_name
result['service_catalog'] = self.service_catalog
return result
@classmethod
def elevated(cls):
def admin(cls):
try:
ctx = cls.current()
cur = cls.current()
request_id = cur.request_id
global_request_id = cur.global_request_id
service_catalog = cur.service_catalog
except RuntimeError:
ctx = None
return cls(ctx, is_admin=True)
request_id = global_request_id = service_catalog = None
return cls(
user_name=CONF.os_admin_username,
user_domain_name=CONF.os_admin_user_domain_name,
project_name=CONF.os_admin_project_name,
project_domain_name=CONF.os_admin_project_domain_name,
is_admin=True,
service_catalog=service_catalog,
request_id=request_id,
global_request_id=global_request_id
)
def current():
return BlazarContext.current()
def elevated():
return BlazarContext.elevated()
def admin():
return BlazarContext.admin()

View File

@ -91,7 +91,7 @@ def enforce(context, action, target, do_raise=True):
init()
credentials = context.to_dict()
credentials = context.to_policy_values()
# Add the exceptions arguments if asked to do a raise
extra = {}

View File

@ -27,6 +27,9 @@ from blazar import tests
PATH_PREFIX = '/v2'
FAKE_PROJECT = '981b767265174c108bc5a61185b748ac'
FAKE_USER = 'b4fdb2fff13545ceb751295096cc18ee'
class APITest(tests.TestCase):
"""Used for unittests tests of Pecan controllers."""
@ -40,11 +43,12 @@ class APITest(tests.TestCase):
def fake_ctx_from_headers(headers):
if not headers:
return context.BlazarContext(
user_id='fake', project_id='fake', roles=['member'])
user_id=FAKE_USER, project_id=FAKE_PROJECT,
roles=['member'])
roles = headers.get('X-Roles', str('member')).split(',')
return context.BlazarContext(
user_id=headers.get('X-User-Id', 'fake'),
project_id=headers.get('X-Project-Id', 'fake'),
user_id=headers.get('X-User-Id', FAKE_USER),
project_id=headers.get('X-Project-Id', FAKE_PROJECT),
auth_token=headers.get('X-Auth-Token', None),
service_catalog=None,
user_name=headers.get('X-User-Name', 'fake'),

View File

@ -19,7 +19,6 @@ import webob
from werkzeug import wrappers
from blazar.api import context as api_context
from blazar import context
from blazar import exceptions
from blazar import tests
@ -33,9 +32,10 @@ class ContextTestCase(tests.TestCase):
'X-Project-Id': uuidsentinel.project_id,
'X-Auth-Token': '111-111-111',
'X-User-Name': 'user_name',
'X-User-Domain-Name': 'user_domain_name',
'X-Project-Name': 'project_name',
'X-Project-Domain-Name': 'project_domain_name',
'X-Roles': 'user_name0, user_name1'}
self.context = self.patch(context, 'BlazarContext')
self.catalog = jsonutils.dump_as_bytes({'nova': 'catalog'})
def test_ctx_from_headers_no_catalog(self):
@ -64,19 +64,24 @@ class ContextTestCaseV1(ContextTestCase):
'/v1/leases',
headers=self.fake_headers,
environ_base=environ_base)
api_context.ctx_from_headers(req.headers)
self.context.assert_called_once_with(
context = api_context.ctx_from_headers(req.headers)
expected = dict(
user_id=uuidsentinel.user_id,
roles=['user_name0',
'user_name1'],
project_name='project_name',
project_domain_name='project_domain_name',
auth_token='111-111-111',
service_catalog={'nova': 'catalog'},
project_id=uuidsentinel.project_id,
user_name='user_name',
user_domain_name='user_domain_name',
request_id='req-' + uuidsentinel.reqid,
global_request_id='req-' + uuidsentinel.globalreqid)
global_request_id='req-' + uuidsentinel.globalreqid
)
for k, v in expected.items():
self.assertEqual(getattr(context, k, None), v)
class ContextTestCaseV2(ContextTestCase):
@ -85,14 +90,19 @@ class ContextTestCaseV2(ContextTestCase):
self.fake_headers['X-Service-Catalog'] = self.catalog
req = webob.Request.blank('/v2/leases')
req.headers = self.fake_headers
api_context.ctx_from_headers(req.headers)
self.context.assert_called_once_with(
context = api_context.ctx_from_headers(req.headers)
expected = dict(
user_id=uuidsentinel.user_id,
roles=['user_name0',
'user_name1'],
project_name='project_name',
project_domain_name='project_domain_name',
auth_token='111-111-111',
service_catalog={'nova': 'catalog'},
project_id=uuidsentinel.project_id,
user_name='user_name')
user_name='user_name',
user_domain_name='user_domain_name'
)
for k, v in expected.items():
self.assertEqual(getattr(context, k, None), v)

View File

@ -28,9 +28,8 @@ def fake_lease(**kw):
'end_date': kw.get('end_date', '2014-02-01 13:37'),
'trust_id': kw.get('trust_id',
'35b17138b3644e6aa1318f3099c5be68'),
'user_id': kw.get('user_id', 'efd8780712d24b389c705f5c2ac427ff'),
'project_id': kw.get('project_id',
'bd9431c18d694ad3803a8d4a6b89fd36'),
'user_id': kw.get('user_id', api.FAKE_USER),
'project_id': kw.get('project_id', api.FAKE_PROJECT),
'reservations': kw.get('reservations', [
{
'resource_id': '1234',

View File

@ -13,14 +13,27 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from oslo_config import cfg
from oslo_config import fixture as conf_fixture
from oslo_utils.fixture import uuidsentinel
from blazar import context
from blazar import tests
CONF = cfg.CONF
class TestBlazarContext(tests.TestCase):
def setUp(self):
super(TestBlazarContext, self).setUp()
self.cfg = self.useFixture(conf_fixture.Config(CONF))
self.cfg.config(os_admin_username='fake-admin')
self.cfg.config(os_admin_user_domain_name='fake-admin-domain')
self.cfg.config(os_admin_project_name='fake-admin-project')
self.cfg.config(os_admin_project_domain_name='fake-admin-domain')
def test_to_dict(self):
ctx = context.BlazarContext(
user_id=111, project_id=222,
@ -33,8 +46,6 @@ class TestBlazarContext(tests.TestCase):
'is_admin_project': True,
'project': 222,
'project_domain': None,
'project_id': 222,
'project_name': None,
'read_only': False,
'request_id': 'req-679033b7-1755-4929-bf85-eb3bfaef7e0b',
'resource_uuid': None,
@ -45,15 +56,9 @@ class TestBlazarContext(tests.TestCase):
'tenant': 222,
'user': 111,
'user_domain': None,
'user_id': 111,
'user_identity': '111 222 - - -',
'user_name': None}
'user_identity': '111 222 - - -'}
self.assertEqual(expected, ctx.to_dict())
def test_elevated_empty(self):
ctx = context.BlazarContext.elevated()
self.assertTrue(ctx.is_admin)
def test_service_catalog_default(self):
ctxt = context.BlazarContext(user_id=uuidsentinel.user_id,
project_id=uuidsentinel.project_id)
@ -69,14 +74,30 @@ class TestBlazarContext(tests.TestCase):
service_catalog=None)
self.assertEqual([], ctxt.service_catalog)
def test_blazar_context_elevated(self):
user_context = context.BlazarContext(
user_id=uuidsentinel.user_id,
project_id=uuidsentinel.project_id, is_admin=False)
self.assertFalse(user_context.is_admin)
def test_admin(self):
ctx = context.admin()
self.assertEqual(ctx.user_name, 'fake-admin')
self.assertEqual(ctx.user_domain_name, 'fake-admin-domain')
self.assertEqual(ctx.project_name, 'fake-admin-project')
self.assertEqual(ctx.project_domain_name, 'fake-admin-domain')
self.assertEqual(ctx.is_admin, True)
admin_context = user_context.elevated()
self.assertFalse(user_context.is_admin)
self.assertTrue(admin_context.is_admin)
self.assertNotIn('admin', user_context.roles)
self.assertIn('admin', admin_context.roles)
def test_admin_nested(self):
"""Test that admin properties take priority over current context."""
request_id = 'req-679033b7-1755-4929-bf85-eb3bfaef7e0b'
service_catalog = ['foo']
ctx = context.BlazarContext(
user_name='fake-user', user_domain_name='fake-user-domain',
project_name='fake-project',
project_domain_name='fake-user-domain',
service_catalog=service_catalog, request_id=request_id)
with ctx:
admin_ctx = context.admin()
self.assertEqual(admin_ctx.user_name, 'fake-admin')
self.assertEqual(admin_ctx.user_domain_name, 'fake-admin-domain')
self.assertEqual(admin_ctx.project_name, 'fake-admin-project')
self.assertEqual(admin_ctx.project_domain_name,
'fake-admin-domain')
self.assertEqual(admin_ctx.is_admin, True)
self.assertEqual(admin_ctx.request_id, request_id)
self.assertEqual(admin_ctx.service_catalog, service_catalog)

View File

@ -52,15 +52,6 @@ class BlazarPolicyTestCase(tests.TestCase):
self.assertRaises(exceptions.PolicyNotAuthorized, policy.enforce,
self.context, action, target)
def test_elevatedpolicy(self):
target = {'user_id': self.context.user_id,
'project_id': self.context.project_id}
action = "blazar:oshosts:get"
self.assertRaises(exceptions.PolicyNotAuthorized, policy.enforce,
self.context, action, target)
elevated_context = self.context.elevated()
self.assertTrue(policy.enforce(elevated_context, action, target))
def test_authorize(self):
@policy.authorize('leases', 'get', ctx=self.context)

View File

@ -38,56 +38,59 @@ class TestCKClient(tests.TestCase):
self.version = '3'
self.username = 'fake_user'
self.user_domain_name = 'fake_user_domain'
self.token = 'fake_token'
self.password = 'fake_pass'
self.tenant_name = 'fake_project'
self.project_name = 'fake_project'
self.project_domain_name = 'fake_project_domain'
self.auth_url = 'fake_url'
self.trust_id = 'fake_trust'
def test_client_from_kwargs(self):
self.ctx.side_effect = RuntimeError
self.keystone.BlazarKeystoneClient(version=self.version,
username=self.username,
password=self.password,
tenant_name=self.tenant_name,
trust_id=self.trust_id,
auth_url=self.auth_url)
self.client.assert_called_once_with(version=self.version,
trust_id=self.trust_id,
username=self.username,
password=self.password,
auth_url=self.auth_url)
self.keystone.BlazarKeystoneClient(
version=self.version,
username=self.username,
password=self.password,
project_name=self.project_name,
trust_id=self.trust_id,
auth_url=self.auth_url)
self.client.assert_called_once_with(
version=self.version,
trust_id=self.trust_id,
username=self.username,
password=self.password,
auth_url=self.auth_url)
def test_client_from_kwargs_and_ctx(self):
self.keystone.BlazarKeystoneClient(version=self.version,
username=self.username,
password=self.password,
tenant_name=self.tenant_name,
auth_url=self.auth_url)
self.client.assert_called_once_with(version=self.version,
tenant_name=self.tenant_name,
endpoint='http://fake.com/',
username=self.username,
password=self.password,
auth_url=self.auth_url,
global_request_id=self.
context.current().
global_request_id)
self.keystone.BlazarKeystoneClient(
version=self.version,
username=self.username,
user_domain_name=self.user_domain_name,
password=self.password,
project_name=self.project_name,
project_domain_name=self.project_domain_name,
auth_url=self.auth_url)
self.client.assert_called_once_with(
version=self.version,
username=self.username,
user_domain_name=self.user_domain_name,
project_name=self.project_name,
project_domain_name=self.project_domain_name,
endpoint='http://fake.com/',
password=self.password,
auth_url=self.auth_url,
global_request_id=self.context.current().global_request_id)
def test_client_from_ctx(self):
self.keystone.BlazarKeystoneClient()
self.client.assert_called_once_with(
version='3',
username=self.ctx().user_name,
user_domain_name=self.ctx().user_domain_name,
token=self.ctx().auth_token,
tenant_name=self.ctx().project_name,
project_name=self.ctx().project_name,
project_domain_name=self.ctx().project_domain_name,
auth_url='http://fake.com/',
endpoint='http://fake.com/',
global_request_id=self.context.current().global_request_id)

View File

@ -65,10 +65,8 @@ class TestTrusts(tests.TestCase):
'global_request_id': self.context.current().global_request_id,
'is_admin': False,
'is_admin_project': True,
'project': self.client().tenant_id,
'project': self.client().project_id,
'project_domain': None,
'project_id': self.client().tenant_id,
'project_name': 'admin',
'read_only': False,
'request_id': ctx.request_id,
'resource_uuid': None,
@ -76,10 +74,8 @@ class TestTrusts(tests.TestCase):
'service_catalog': ctx.service_catalog,
'show_deleted': False,
'system_scope': None,
'tenant': self.client().tenant_id,
'user': None,
'user_domain': None,
'user_id': None}
'user_domain': None}
self.assertDictContainsSubset(fake_ctx_dict, ctx.to_dict())
def test_use_trust_auth_dict(self):

View File

@ -101,7 +101,9 @@ class BlazarKeystoneClient(object):
kwargs.setdefault('version', cfg.CONF.keystone_client_version)
if ctx is not None:
kwargs.setdefault('username', ctx.user_name)
kwargs.setdefault('tenant_name', ctx.project_name)
kwargs.setdefault('user_domain_name', ctx.user_domain_name)
kwargs.setdefault('project_name', ctx.project_name)
kwargs.setdefault('project_domain_name', ctx.project_domain_name)
kwargs.setdefault('global_request_id', ctx.global_request_id)
if not kwargs.get('auth_url'):
kwargs['auth_url'] = base.url_for(
@ -121,8 +123,8 @@ class BlazarKeystoneClient(object):
# NOTE(dbelova): we need this checking to support current
# keystoneclient: token can only be scoped now to either
# a trust or project, not both.
if kwargs.get('trust_id') and kwargs.get('tenant_name'):
kwargs.pop('tenant_name')
if kwargs.get('trust_id') and kwargs.get('project_name'):
kwargs.pop('project_name')
try:
# NOTE(n.s.): we shall remove this try: except: clause when

View File

@ -74,7 +74,7 @@ class ContextEndpointHandler(object):
method = getattr(self.__endpoint, name)
def run_method(__ctx, **kwargs):
with context.BlazarContext(**__ctx):
with context.BlazarContext.from_dict(__ctx):
return method(**kwargs)
return run_method

View File

@ -29,9 +29,8 @@ def create_trust():
client = keystone.BlazarKeystoneClient()
trustee_id = keystone.BlazarKeystoneClient(
username=CONF.os_admin_username,
password=CONF.os_admin_password,
tenant_name=CONF.os_admin_project_name).user_id
ctx=context.admin()).user_id
ctx = context.current()
trust = client.trusts.create(trustor_user=ctx.user_id,
@ -53,31 +52,26 @@ def create_ctx_from_trust(trust_id):
"""Return context built from given trust."""
ctx = context.current()
ctx = context.BlazarContext(
user_name=CONF.os_admin_username,
project_name=CONF.os_admin_project_name,
request_id=ctx.request_id,
global_request_id=ctx.global_request_id
)
auth_url = "%s://%s:%s" % (CONF.os_auth_protocol,
base.get_os_auth_host(CONF),
CONF.os_auth_port)
if CONF.os_auth_prefix:
auth_url += "/%s" % CONF.os_auth_prefix
client = keystone.BlazarKeystoneClient(
password=CONF.os_admin_password,
trust_id=trust_id,
auth_url=auth_url,
ctx=ctx,
ctx=context.admin(),
)
# use 'with ctx' statement in the place you need context from trust
return context.BlazarContext(
user_name=ctx.user_name,
project_name=ctx.project_name,
user_domain_name=ctx.user_domain_name,
auth_token=client.auth_token,
service_catalog=client.service_catalog.catalog['catalog'],
project_id=client.tenant_id,
project_id=client.project_id,
request_id=ctx.request_id,
global_request_id=ctx.global_request_id
)

View File

@ -0,0 +1,5 @@
---
fixes:
- |
Allows users of multiple Keystone domains to create leases; previously only users
and projects in the default domain could use Blazar.