Merge "Add project hierarchical tree check when Keystone start"
This commit is contained in:
commit
a5afd224c0
|
@ -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)
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue