Properly normalize domain ids in flask

Previously domain_id normalization was done (in webob) resulting
in possibly one of four results (ref['domain_id'] is changed):

  * Domain ID present in ref -> no change to ref

  * Domain ID not present, domain scoped token ->
    ref['domain_id'] = scope domain id

  * Domain ID not present, "admin" token -> raise ValidationError

  * Domain ID not present, project scoped token -> default domain
    [Deprecated functionality]

In flask, only the first case worked. This change corrects the behavior
and adds a test to ensure proper data is extracted from oslo.context.

Change-Id: Iacb502a2aa3fe633f74c7e19e13c46f4f85e55db
Closes-Bug: #1793027
This commit is contained in:
morgan fainberg 2018-09-17 14:59:08 -07:00
parent 4532b97b03
commit c96c7fd03b
2 changed files with 99 additions and 1 deletions

View File

@ -23,6 +23,7 @@ from flask import blueprints
import flask_restful
import flask_restful.utils
from oslo_log import log
from oslo_log import versionutils
from oslo_serialization import jsonutils
from pycadf import cadftaxonomy as taxonomy
from pycadf import host
@ -38,6 +39,7 @@ from keystone.common.rbac_enforcer import enforcer
from keystone.common import utils
import keystone.conf
from keystone import exception
from keystone.i18n import _
# NOTE(morgan): Capture the relevant part of the flask url route rule for
@ -937,7 +939,37 @@ class ResourceBase(flask_restful.Resource):
def _normalize_domain_id(cls, ref):
"""Fill in domain_id if not specified in a v3 call."""
if not ref.get('domain_id'):
ref['domain_id'] = cls._get_domain_id_from_token()
oslo_ctx = flask.request.environ.get(
context.REQUEST_CONTEXT_ENV, None)
if oslo_ctx and oslo_ctx.domain_id:
# Domain Scoped Token Scenario.
ref['domain_id'] = oslo_ctx.domain_id
elif oslo_ctx.is_admin:
# Legacy "shared" admin token Scenario
raise exception.ValidationError(
_('You have tried to create a resource using the admin '
'token. As this token is not within a domain you must '
'explicitly include a domain for this resource to '
'belong to.'))
else:
# TODO(henry-nash): We should issue an exception here since if
# a v3 call does not explicitly specify the domain_id in the
# entity, it should be using a domain scoped token. However,
# the current tempest heat tests issue a v3 call without this.
# This is raised as bug #1283539. Once this is fixed, we
# should remove the line below and replace it with an error.
#
# Ahead of actually changing the code to raise an exception, we
# issue a deprecation warning.
versionutils.report_deprecated_feature(
LOG,
'Not specifying a domain during a create user, group or '
'project call, and relying on falling back to the '
'default domain, is deprecated as of Liberty. There is no '
'plan to remove this compatibility, however, future API '
'versions may remove this, so please specify the domain '
'explicitly or use a domain-scoped token.')
ref['domain_id'] = CONF.identity.default_domain_id
return ref

View File

@ -19,13 +19,18 @@ from oslo_policy import policy
from oslo_serialization import jsonutils
from testtools import matchers
from keystone.common import context
from keystone.common import json_home
from keystone.common import rbac_enforcer
import keystone.conf
from keystone import exception
from keystone.server.flask import common as flask_common
from keystone.tests.unit import rest
CONF = keystone.conf.CONF
class _TestResourceWithCollectionInfo(flask_common.ResourceBase):
collection_key = 'arguments'
member_key = 'argument'
@ -544,3 +549,64 @@ class TestKeystoneFlaskCommon(rest.RestfulTestCase):
for rel in json_home_data:
self.assertThat(resp_data['resources'][rel],
matchers.Equals(json_home_data[rel]))
def test_normalize_domain_id_extracts_domain_id_if_needed(self):
self._setup_flask_restful_api()
blueprint_prefix = self.restful_api._blueprint_url_prefix.rstrip('/')
url = ''.join([blueprint_prefix, '/arguments'])
headers = {'X-Auth-Token': self._get_token()}
ref_with_domain_id = {'domain_id': uuid.uuid4().hex}
ref_without_domain_id = {}
with self.test_client() as c:
# Make a dummy request.. ANY request is fine to push the whole
# context stack.
c.get('%s/%s' % (url, uuid.uuid4().hex), headers=headers,
expected_status_code=404)
oslo_context = flask.request.environ[
context.REQUEST_CONTEXT_ENV]
# Normal Project Scope Form
# ---------------------------
# test that _normalize_domain_id does something sane
domain_id = ref_with_domain_id['domain_id']
# Ensure we don't change the domain if it is specified
flask_common.ResourceBase._normalize_domain_id(ref_with_domain_id)
self.assertEqual(domain_id, ref_with_domain_id['domain_id'])
# Ensure (deprecated) we add default domain if needed
flask_common.ResourceBase._normalize_domain_id(
ref_without_domain_id)
self.assertEqual(
CONF.identity.default_domain_id,
ref_without_domain_id['domain_id'])
ref_without_domain_id.clear()
# Domain Scoped Form
# --------------------
# Just set oslo_context domain_id to a value. This is how we
# communicate domain scope. No need to explicitly
# do a domain-scoped request, this is a synthetic text anyway
oslo_context.domain_id = uuid.uuid4().hex
# Ensure we don't change the domain if it is specified
flask_common.ResourceBase._normalize_domain_id(ref_with_domain_id)
self.assertEqual(domain_id, ref_with_domain_id['domain_id'])
flask_common.ResourceBase._normalize_domain_id(
ref_without_domain_id)
self.assertEqual(oslo_context.domain_id,
ref_without_domain_id['domain_id'])
ref_without_domain_id.clear()
# "Admin" Token form
# -------------------
# Explicitly set "is_admin" to true, no new request is needed
# as we simply check "is_admin" value everywhere
oslo_context.is_admin = True
oslo_context.domain_id = None
# Ensure we don't change the domain if it is specified
flask_common.ResourceBase._normalize_domain_id(ref_with_domain_id)
self.assertEqual(domain_id, ref_with_domain_id['domain_id'])
# Ensure we raise an appropriate exception with the inferred
# domain_id
self.assertRaises(exception.ValidationError,
flask_common.ResourceBase._normalize_domain_id,
ref=ref_without_domain_id)