Merge "Add project hierarchical tree check when Keystone start"

This commit is contained in:
Zuul 2018-07-25 00:53:25 +00:00 committed by Gerrit Code Review
commit a5afd224c0
8 changed files with 136 additions and 0 deletions

View File

@ -350,6 +350,16 @@ class InvalidLimit(Forbidden):
message_format = _("Invalid resource limit: %(reason)s.")
class LimitTreeExceedError(Exception):
def __init__(self, project_id, max_limit_depth):
super(LimitTreeExceedError, self).__init__(_(
"Keystone cannot start due to project hierarchical depth in the "
"current deployment (project_ids: %(project_id)s) exceeds the "
"enforcement model's maximum limit of %(max_limit_depth)s. Please "
"use a different enforcement model to correct the issue."
) % {'project_id': project_id, 'max_limit_depth': max_limit_depth})
class NotFound(Error):
message_format = _("Could not find: %(target)s.")
code = int(http_client.NOT_FOUND)

View File

@ -39,6 +39,11 @@ class Manager(manager.Manager):
self.enforcement_model = base.load_driver(
CONF.unified_limit.enforcement_model)
def check_project_depth(self):
"""Check project depth if satisfy current enforcement model or not."""
PROVIDERS.resource_api.check_project_depth(
self.enforcement_model.MAX_PROJECT_TREE_DEPTH)
def _assert_resource_exist(self, unified_limit, target):
try:
service_id = unified_limit.get('service_id')

View File

@ -261,3 +261,14 @@ class ResourceDriverBase(object):
"""
raise exception.NotImplemented() # pragma: no cover
def check_project_depth(self, max_depth):
"""Check the projects depth in the backend whether exceed the limit.
:param max_depth: the limit depth that project depth should not exceed.
:type max_depth: integer
:returns: the exceeded project's id or None if no exceeding.
"""
raise exception.NotImplemented() # pragma: no cover

View File

@ -13,6 +13,7 @@
from oslo_log import log
from six import text_type
from sqlalchemy import orm
from sqlalchemy.sql import expression
from keystone.common import driver_hints
from keystone.common import sql
@ -275,6 +276,66 @@ class Resource(base.ResourceDriverBase):
'deleted.', project_id)
query.delete(synchronize_session=False)
def check_project_depth(self, max_depth):
with sql.session_for_read() as session:
obj_list = []
# Using db table self outerjoin to find the project descendants.
#
# We'll only outerjoin the project table (max_depth + 1) times to
# check whether current project tree exceed the max depth limit.
#
# Note one more time here is for project act as domain.
#
# for example:
# If max_depth is 2, we will take the outerjoin 3 times, then the
# SQL result may be like:
# +----+-------------+-------------+-------------+-------------+
# | No | project1_id | project2_id | project3_id | project4_id |
# +----+-------------+-------------+-------------+-------------+
# | 1 | project_a | | | |
# +----+-------------+-------------+-------------+-------------+
# | 2 | domain_x | project_a | | |
# +----+-------------+-------------+-------------+-------------+
# | 3 | project_b | project_c | | |
# +----+-------------+-------------+-------------+-------------+
# | 4 | domain_x | project_b | project_c | |
# +----+-------------+-------------+-------------+-------------+
# | 5 | project_d | project_e | project_f | |
# +----+-------------+-------------+-------------+-------------+
# | 6 | domain_x | project_d | project_e | project_f |
# +----+-------------+-------------+-------------+-------------+
#
# project1 is the root. It is a project or a domain. If project1 is
# a project, there must exist a line that project1 is its domain.
#
# we got 6 lines here.
#
# 1). the 1, 2 line means project project_a has no child, the depth
# is 1.
# 2). the 3, 4 line means project project_a has a child, the depth
# is 2.
# 3). the 5, 6 line means project project_a has a grandchild, the
# depth is 3. this tree hit the max depth.
# So we can see that if column "project4_id" has value, it means
# some trees hit the max depth limit.
outerjoin_obj_number = max_depth + 2
for _ in range(outerjoin_obj_number):
obj_list.append(orm.aliased(Project))
query = session.query(*obj_list)
outerjoin_count = max_depth + 1
for index in range(outerjoin_count):
query = query.outerjoin(
obj_list[index + 1],
obj_list[index].id == obj_list[index + 1].parent_id)
exceeded_lines = query.filter(
obj_list[-1].id != expression.null())
if exceeded_lines:
return [line[max_depth + 1].id for line in exceeded_lines]
class Project(sql.ModelBase, sql.ModelDictMixinWithExtras):
# NOTE(henry-nash): From the manager and above perspective, the domain_id

View File

@ -974,6 +974,15 @@ class Manager(manager.Manager):
self.update_project(project_id, project)
notifications.Audit.deleted(self._PROJECT_TAG, tag)
def check_project_depth(self, max_depth=None):
"""Check project depth whether greater than input or not."""
if max_depth:
exceeded_project_ids = self.driver.check_project_depth(max_depth)
if exceeded_project_ids:
raise exception.LimitTreeExceedError(exceeded_project_ids,
max_depth)
MEMOIZE_CONFIG = cache.get_memoization_decorator(group='domain_config')

View File

@ -9,6 +9,9 @@
# 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 sys
from oslo_log import log
from keystone import application_credential
from keystone import assignment
@ -18,6 +21,7 @@ from keystone.common import cache
from keystone.common import provider_api
from keystone import credential
from keystone import endpoint_policy
from keystone import exception
from keystone import federation
from keystone import identity
from keystone import limit
@ -28,6 +32,8 @@ from keystone import revoke
from keystone import token
from keystone import trust
LOG = log.getLogger(__name__)
def load_backends():
@ -55,6 +61,13 @@ def load_backends():
# NOTE(morgan): lock the APIs, these should only ever be instantiated
# before running keystone.
provider_api.ProviderAPIs.lock_provider_registry()
try:
# Check project depth before start process. If fail, Keystone will not
# start.
drivers['unified_limit_api'].check_project_depth()
except exception.LimitTreeExceedError as e:
LOG.critical(e)
sys.exit(1)
auth.core.load_auth_methods()

View File

@ -727,6 +727,29 @@ class SqlIdentity(SqlTests,
self.assertNotEqual(len(first_call_users), len(second_call_users))
self.assertEqual(first_call_counter, counter.calls)
def test_check_project_depth(self):
# create a 3 level project tree
ref = unit.new_project_ref(domain_id=CONF.identity.default_domain_id)
PROVIDERS.resource_api.create_project(ref['id'], ref)
ref_1 = unit.new_project_ref(domain_id=CONF.identity.default_domain_id,
parent_id=ref['id'])
PROVIDERS.resource_api.create_project(ref_1['id'], ref_1)
ref_2 = unit.new_project_ref(domain_id=CONF.identity.default_domain_id,
parent_id=ref_1['id'])
PROVIDERS.resource_api.create_project(ref_2['id'], ref_2)
# if max_depth is None or >= current project depth, return nothing.
resp = PROVIDERS.resource_api.check_project_depth(max_depth=None)
self.assertIsNone(resp)
resp = PROVIDERS.resource_api.check_project_depth(max_depth=3)
self.assertIsNone(resp)
resp = PROVIDERS.resource_api.check_project_depth(max_depth=4)
self.assertIsNone(resp)
# if max_depth < current project depth, raise LimitTreeExceedError
self.assertRaises(exception.LimitTreeExceedError,
PROVIDERS.resource_api.check_project_depth,
2)
class SqlTrust(SqlTests, trust_tests.TrustTests):

View File

@ -17,6 +17,10 @@ features:
If a newly created project results in a project tree depth greater than 2, a
`403 Forbidden` error will be raised.
When try to use this model but the project depth exceed 2 already, Keystone
process will fail to start. Operators should chose another available model
to fix the issue first.
- >
[`blueprint strict-two-level-model <https://blueprints.launchpad.net/keystone/+spec/strict-two-level-model>`_]
The `project_id` filter is added for listing limits. This filter is used