diff --git a/keystone/access_rules_config/__init__.py b/keystone/access_rules_config/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/keystone/access_rules_config/backends/__init__.py b/keystone/access_rules_config/backends/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/keystone/access_rules_config/backends/base.py b/keystone/access_rules_config/backends/base.py new file mode 100644 index 0000000000..7fd7128b01 --- /dev/null +++ b/keystone/access_rules_config/backends/base.py @@ -0,0 +1,31 @@ +# Copyright 2019 SUSE Linux GmbH +# +# 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 abc + +import six + + +@six.add_metaclass(abc.ABCMeta) +class AccessRulesConfigDriverBase(object): + + @abc.abstractmethod + def list_access_rules_config(self, service=None): + """List access rules config.""" + raise NotImplementedError() # pragma: no cover + + @abc.abstractmethod + def check_access_rule(self, service, request_path, request_method): + """Check if an access rule exists in config.""" + raise NotImplementedError() # pragma: no cover diff --git a/keystone/access_rules_config/backends/json.py b/keystone/access_rules_config/backends/json.py new file mode 100644 index 0000000000..ae38abdff8 --- /dev/null +++ b/keystone/access_rules_config/backends/json.py @@ -0,0 +1,160 @@ +# Copyright 2019 SUSE Linux GmbH +# +# 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 re + +from oslo_log import log +from oslo_serialization import jsonutils + +from keystone.access_rules_config.backends import base +import keystone.conf +from keystone import exception + +CONF = keystone.conf.CONF +LOG = log.getLogger(__name__) + + +class AccessRulesConfig(base.AccessRulesConfigDriverBase): + """This backend reads the access rules from a JSON file on disk. + + The format of the file is a mapping from service type to rules for that + service type. For example:: + + { + "identity": [ + { + "path": "/v3/users", + "method": "GET" + }, + { + "path": "/v3/users", + "method": "POST" + }, + { + "path": "/v3/users/*", + "method": "GET" + }, + { + "path": "/v3/users/*", + "method": "PATCH" + }, + { + "path": "/v3/users/*", + "method": "DELETE" + } + ... + ], + "image": [ + { + "path": "/v2/images", + "method": "GET" + }, + ... + ], + ... + } + + This will be transmuted in memory to a hash map that looks like this:: + + { + "identity": { + "GET": [ + { + "path": "/v3/users" + }, + { + "path": "/v3/users/*" + } + ... + ], + "POST": [ ... ] + }, + ... + } + + The path may include a wildcard like '*' or '**' or a named wildcard like + {server_id}. An application credential access rule validation request for + a path like "/v3/users/uuid" will match with a configured access rule like + "/v3/users/*" or "/v3/users/{user_id}", and a request for a path like + "/v3/users/uuid/application_credentials/uuid" will match with a configured + access rule like "/v3/users/**". + + """ + + def __init__(self): + super(AccessRulesConfig, self).__init__() + access_rules_file = CONF.access_rules_config.rules_file + self.access_rules = dict() + self.access_rules_json = dict() + try: + with open(access_rules_file, "rb") as f: + self.access_rules_json = jsonutils.load(f) + except IOError: + LOG.warning('No config file found for access rules, application' + ' credential access rules will be unavailable.') + return + except ValueError as e: + raise exception.AccessRulesConfigFileError(error=e) + + for service, rules in self.access_rules_json.items(): + self.access_rules[service] = dict() + for rule in rules: + try: + self.access_rules[service].setdefault( + rule['method'], []).append({ + 'path': rule['path'] + }) + except KeyError as e: + raise exception.AccessRulesConfigFileError(error=e) + + def _path_matches(self, request_path, path_pattern): + # The fnmatch module doesn't provide the ability to match * versus **, + # so convert to regex. + # replace {tags} with * + pattern = r'{[^}]*}' + replace = r'*' + path_regex = re.sub(pattern, replace, path_pattern) + # temporarily sub out ** + pattern = r'([^\*]*)\*\*([^\*]*)' + replace = r'\1%TMP%\2' + path_regex = re.sub(pattern, replace, path_regex) + # replace * with [^\/]* (all except /) + pattern = r'([^\*]?)\*($|[^\*])' + replace = r'\1[^\/]*\2' + path_regex = re.sub(pattern, replace, path_regex) + # replace ** with .* (includes /) + pattern = r'%TMP%' + replace = '.*' + path_regex = re.sub(pattern, replace, path_regex) + path_regex = r'^%s$' % path_regex + regex = re.compile(path_regex) + return regex.match(request_path) + + def list_access_rules_config(self, service=None): + """List access rules config in human readable form.""" + if service: + if service not in self.access_rules_json: + raise exception.AccessRulesConfigNotFound(service=service) + return {service: self.access_rules_json[service]} + return self.access_rules_json + + def check_access_rule(self, service, request_path, request_method): + """Check if an access rule exists in config.""" + if (service in self.access_rules + and request_method in self.access_rules[service]): + rules = self.access_rules[service][request_method] + for rule in rules: + if self._path_matches(request_path, rule['path']): + return True + return False diff --git a/keystone/conf/__init__.py b/keystone/conf/__init__.py index b6aa8c02e1..d2374d63c9 100644 --- a/keystone/conf/__init__.py +++ b/keystone/conf/__init__.py @@ -20,6 +20,7 @@ import oslo_messaging from oslo_middleware import cors from osprofiler import opts as profiler +from keystone.conf import access_rules_config from keystone.conf import application_credential from keystone.conf import assignment from keystone.conf import auth @@ -58,6 +59,7 @@ CONF = cfg.CONF conf_modules = [ + access_rules_config, application_credential, assignment, auth, diff --git a/keystone/conf/access_rules_config.py b/keystone/conf/access_rules_config.py new file mode 100644 index 0000000000..f502c0ad7c --- /dev/null +++ b/keystone/conf/access_rules_config.py @@ -0,0 +1,68 @@ +# Copyright 2019 SUSE Linux GmbH +# +# 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 keystone.conf import utils + + +driver = cfg.StrOpt( + 'driver', + default='json', + help=utils.fmt(""" +Entry point for the access rules config backend driver in the +`keystone.access_rules_config` namespace. Keystone only provides a `json` +driver, so there is no reason to change this unless you are providing a custom +entry point. +""")) + +caching = cfg.BoolOpt( + 'caching', + default=True, + help=utils.fmt(""" +Toggle for access rules caching. This has no effect unless global caching is +enabled. +""")) + +cache_time = cfg.IntOpt( + 'cache_time', + help=utils.fmt(""" +Time to cache access rule data in seconds. This has no effect unless global +caching is enabled. +""")) + +rules_file = cfg.StrOpt( + 'rules_file', + default='/etc/keystone/access_rules.json', + help=utils.fmt(""" +Path to access rules configuration. If not present, no access rule +configuration will be loaded and application credential access rules will be +unavailable. +""")) + +GROUP_NAME = __name__.split('.')[-1] +ALL_OPTS = [ + driver, + caching, + cache_time, + rules_file, +] + + +def register_opts(conf): + conf.register_opts(ALL_OPTS, group=GROUP_NAME) + + +def list_opts(): + return {GROUP_NAME: ALL_OPTS} diff --git a/keystone/exception.py b/keystone/exception.py index 20f314be13..b85878b7a5 100644 --- a/keystone/exception.py +++ b/keystone/exception.py @@ -545,6 +545,11 @@ class ApplicationCredentialNotFound(NotFound): "%(application_credential_id)s.") +class AccessRulesConfigNotFound(NotFound): + message_format = _( + "Could not find access rules config for service %(service)s.") + + class Conflict(Error): message_format = _("Conflict occurred attempting to store %(type)s -" " %(details)s.") @@ -705,3 +710,8 @@ class CacheDeserializationError(Exception): 'obj': obj, 'data': data } ) + + +class AccessRulesConfigFileError(UnexpectedError): + debug_message_format = _( + 'Could not parse access rules config file: %(error)s') diff --git a/keystone/tests/unit/access_rules_config/__init__.py b/keystone/tests/unit/access_rules_config/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/keystone/tests/unit/access_rules_config/backends/__init__.py b/keystone/tests/unit/access_rules_config/backends/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/keystone/tests/unit/access_rules_config/backends/test_json.py b/keystone/tests/unit/access_rules_config/backends/test_json.py new file mode 100644 index 0000000000..d34ab6a11a --- /dev/null +++ b/keystone/tests/unit/access_rules_config/backends/test_json.py @@ -0,0 +1,84 @@ +# Copyright 2019 SUSE Linux GmbH +# +# 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 uuid + +from keystone.access_rules_config.backends import json as json_driver +from keystone import exception +from keystone.tests import unit +from keystone.tests.unit.ksfixtures import access_rules_config +from keystone.tests.unit.ksfixtures import temporaryfile + + +class JSONDriverTestCase(unit.TestCase): + """Tests for validating the access rules config driver.""" + + def setUp(self): + super(JSONDriverTestCase, self).setUp() + rules_file = '%s/access_rules.json' % unit.TESTCONF + self.useFixture(access_rules_config.AccessRulesConfig( + self.config_fixture, rules_file=rules_file)) + self.driver = json_driver.AccessRulesConfig() + + def test_invalid_json_raises_error(self): + tmpfile = self.useFixture(temporaryfile.SecureTempFile()) + invalid_access_rules = tmpfile.file_name + with open(invalid_access_rules, 'w') as f: + f.write("This is an invalid data") + self.useFixture(access_rules_config.AccessRulesConfig( + self.config_fixture, rules_file=invalid_access_rules)) + self.assertRaises(exception.AccessRulesConfigFileError, + json_driver.AccessRulesConfig) + + def test_list_access_rules_config(self): + rules = self.driver.list_access_rules_config() + self.assertIn('identity', rules) + self.assertIn('image', rules) + + def test_list_access_rules_config_for_service(self): + rules = self.driver.list_access_rules_config(service='image') + self.assertNotIn('identity', rules) + self.assertIn('image', rules) + + def test_check_access_rule(self): + result = self.driver.check_access_rule('identity', '/v3/users', 'GET') + self.assertTrue(result) + userid = uuid.uuid4().hex + check_path = '/v3/users/%(userid)s' % {'userid': userid} + result = self.driver.check_access_rule('identity', check_path, 'GET') + self.assertTrue(result) + img = uuid.uuid4().hex + memb = uuid.uuid4().hex + check_path = '/v2/images/%(img)s/members/%(memb)s' % {'img': img, + 'memb': memb} + result = self.driver.check_access_rule('image', check_path, 'PUT') + self.assertTrue(result) + result = self.driver.check_access_rule('image', '/servers', 'GET') + self.assertFalse(result) + result = self.driver.check_access_rule('glance', '/v2/images', 'GET') + self.assertFalse(result) + result = self.driver.check_access_rule('image', 'images', 'POST') + self.assertFalse(result) + projectid = uuid.uuid4().hex + check_path = '/v3/%(projectid)s/volumes' % {'projectid': projectid} + result = self.driver.check_access_rule('block-storage', check_path, + 'GET') + self.assertTrue(result) + check_path = '/v2/%(projectid)s/volumes' % {'projectid': projectid} + result = self.driver.check_access_rule('block-storage', check_path, + 'GET') + self.assertFalse(result) + result = self.driver.check_access_rule('compute', '/v2.1/servers', + 'GET') + self.assertTrue(result) diff --git a/keystone/tests/unit/config_files/access_rules.json b/keystone/tests/unit/config_files/access_rules.json new file mode 100644 index 0000000000..91ae31c3cb --- /dev/null +++ b/keystone/tests/unit/config_files/access_rules.json @@ -0,0 +1,890 @@ +{ + "identity": [ + { + "path": "/v3/users/*/application_credentials", + "method": "POST" + }, + { + "path": "/v3/users/*/application_credentials", + "method": "GET" + }, + { + "path": "/v3/users/*/application_credentials/*", + "method": "GET" + }, + { + "path": "/v3/users/*/application_credentials/*", + "method": "DELETE" + }, + { + "path": "/v3/auth/catalog", + "method": "GET" + }, + { + "path": "/v3/auth/projects", + "method": "GET" + }, + { + "path": "/v3/auth/domains", + "method": "GET" + }, + { + "path": "/v3/auth/system", + "method": "GET" + }, + { + "path": "/v3/credentials", + "method": "POST" + }, + { + "path": "/v3/credentials", + "method": "GET" + }, + { + "path": "/v3/credentials/*", + "method": "GET" + }, + { + "path": "/v3/credentials/*", + "method": "PATCH" + }, + { + "path": "/v3/credentials/*", + "method": "DELETE" + }, + { + "path": "/v3/domains/config/default", + "method": "GET" + }, + { + "path": "/v3/domains/config/*/default", + "method": "GET" + }, + { + "path": "/v3/domains/config/*/*/default", + "method": "GET" + }, + { + "path": "/v3/domains/*/config/*/*", + "method": "GET" + }, + { + "path": "/v3/domains/*/config/*/*", + "method": "PATCH" + }, + { + "path": "/v3/domains/*/config/*/*", + "method": "DELETE" + }, + { + "path": "/v3/domains/*/config/*", + "method": "GET" + }, + { + "path": "/v3/domains/*/config/*", + "method": "PATCH" + }, + { + "path": "/v3/domains/*/config/*", + "method": "DELETE" + }, + { + "path": "/v3/domains/*/config", + "method": "PUT" + }, + { + "path": "/v3/domains/*/config", + "method": "GET" + }, + { + "path": "/v3/domains/*/config", + "method": "PATCH" + }, + { + "path": "/v3/domains/*/config", + "method": "DELETE" + }, + { + "path": "/v3/domains", + "method": "GET" + }, + { + "path": "/v3/domains", + "method": "POST" + }, + { + "path": "/v3/domains/*", + "method": "GET" + }, + { + "path": "/v3/domains/*", + "method": "PATCH" + }, + { + "path": "/v3/domains/*", + "method": "DELETE" + }, + { + "path": "/v3/groups", + "method": "GET" + }, + { + "path": "/v3/groups", + "method": "POST" + }, + { + "path": "/v3/groups/*", + "method": "GET" + }, + { + "path": "/v3/groups/*", + "method": "PATCH" + }, + { + "path": "/v3/groups/*", + "method": "DELETE" + }, + { + "path": "/v3/groups/*/users", + "method": "GET" + }, + { + "path": "/v3/groups/*/users/*", + "method": "PUT" + }, + { + "path": "/v3/groups/*/users/*", + "method": "HEAD" + }, + { + "path": "/v3/groups/*/users/*", + "method": "DELETE" + }, + { + "path": "/v3/OS-INHERIT/domains/*/users/*/roles/*/inherited_to_projects", + "method": "PUT" + }, + { + "path": "/v3/OS-INHERIT/domains/*/groups/*/roles/*/inherited_to_projects", + "method": "PUT" + }, + { + "path": "/v3/OS-INHERIT/domains/*/users/*/roles/inherited_to_projects", + "method": "GET" + }, + { + "path": "/v3/OS-INHERIT/domains/*/groups/*/roles/inherited_to_projects", + "method": "GET" + }, + { + "path": "/v3/OS-INHERIT/domains/*/users/*/roles/*/inherited_to_projects", + "method": "HEAD" + }, + { + "path": "/v3/OS-INHERIT/domains/*/groups/*/roles/*/inherited_to_projects", + "method": "HEAD" + }, + { + "path": "/v3/OS-INHERIT/domains/*/users/*/roles/*/inherited_to_projects", + "method": "DELETE" + }, + { + "path": "/v3/OS-INHERIT/domains/*/groups/*/roles/*/inherited_to_projects", + "method": "DELETE" + }, + { + "path": "/v3/OS-INHERIT/projects/*/users/*/roles/*/inherited_to_projects", + "method": "PUT" + }, + { + "path": "/v3/OS-INHERIT/projects/*/groups/*/roles/*/inherited_to_projects", + "method": "PUT" + }, + { + "path": "/v3/OS-INHERIT/projects/*/users/*/roles/*/inherited_to_projects", + "method": "HEAD" + }, + { + "path": "/v3/OS-INHERIT/projects/*/groups/*/roles/*/inherited_to_projects", + "method": "HEAD" + }, + { + "path": "/v3/OS-INHERIT/projects/*/users/*/roles/*/inherited_to_projects", + "method": "DELETE" + }, + { + "path": "/v3/OS-INHERIT/projects/*/groups/*/roles/*/inherited_to_projects", + "method": "DELETE" + }, + { + "path": "/v3/role_assignments", + "method": "GET" + }, + { + "path": "/v3/auth/tokens/OS-PKI/revoked", + "method": "GET" + }, + { + "path": "/v3/policies", + "method": "POST" + }, + { + "path": "/v3/policies", + "method": "GET" + }, + { + "path": "/v3/policies/*", + "method": "GET" + }, + { + "path": "/v3/policies/*", + "method": "PATCH" + }, + { + "path": "/v3/policies/*", + "method": "DELETE" + }, + { + "path": "/v3/projects/*/tags", + "method": "GET" + }, + { + "path": "/v3/projects/*/tags", + "method": "PUT" + }, + { + "path": "/v3/projects/*/tags", + "method": "DELETE" + }, + { + "path": "/v3/projects/*/tags/*", + "method": "GET" + }, + { + "path": "/v3/projects/*/tags/*", + "method": "PUT" + }, + { + "path": "/v3/projects/*/tags/*", + "method": "DELETE" + }, + { + "path": "/v3/projects", + "method": "GET" + }, + { + "path": "/v3/projects", + "method": "POST" + }, + { + "path": "/v3/projects/*", + "method": "GET" + }, + { + "path": "/v3/projects/*", + "method": "PATCH" + }, + { + "path": "/v3/projects/*", + "method": "DELETE" + }, + { + "path": "/v3/regions/*", + "method": "GET" + }, + { + "path": "/v3/regions/*", + "method": "PATCH" + }, + { + "path": "/v3/regions/*", + "method": "DELETE" + }, + { + "path": "/v3/regions", + "method": "GET" + }, + { + "path": "/v3/regions", + "method": "POST" + }, + { + "path": "/v3/roles", + "method": "GET" + }, + { + "path": "/v3/roles", + "method": "POST" + }, + { + "path": "/v3/roles/*", + "method": "GET" + }, + { + "path": "/v3/roles/*", + "method": "PATCH" + }, + { + "path": "/v3/roles/*", + "method": "DELETE" + }, + { + "path": "/v3/domains/*/groups/*/roles", + "method": "GET" + }, + { + "path": "/v3/domains/*/groups/*/roles/*", + "method": "PUT" + }, + { + "path": "/v3/domains/*/groups/*/roles/*", + "method": "HEAD" + }, + { + "path": "/v3/domains/*/groups/*/roles/*", + "method": "DELETE" + }, + { + "path": "/v3/domains/*/users/*/roles", + "method": "GET" + }, + { + "path": "/v3/domains/*/users/*/roles/*", + "method": "PUT" + }, + { + "path": "/v3/domains/*/users/*/roles/*", + "method": "HEAD" + }, + { + "path": "/v3/domains/*/users/*/roles/*", + "method": "DELETE" + }, + { + "path": "/v3/projects/*/groups/*/roles", + "method": "GET" + }, + { + "path": "/v3/projects/*/groups/*/roles/*", + "method": "PUT" + }, + { + "path": "/v3/projects/*/groups/*/roles/*", + "method": "HEAD" + }, + { + "path": "/v3/projects/*/groups/*/roles/*", + "method": "DELETE" + }, + { + "path": "/v3/projects/*/users/*/roles", + "method": "GET" + }, + { + "path": "/v3/projects/*/users/*/roles/*", + "method": "PUT" + }, + { + "path": "/v3/projects/*/users/*/roles/*", + "method": "HEAD" + }, + { + "path": "/v3/projects/*/users/*/roles/*", + "method": "DELETE" + }, + { + "path": "/v3/roles/*/implies", + "method": "GET" + }, + { + "path": "/v3/roles/*/implies/*", + "method": "PUT" + }, + { + "path": "/v3/roles/*/implies/*", + "method": "GET" + }, + { + "path": "/v3/roles/*/implies/*", + "method": "HEAD" + }, + { + "path": "/v3/roles/*/implies/*", + "method": "DELETE" + }, + { + "path": "/v3/role_assignments", + "method": "GET" + }, + { + "path": "/v3/role_inferences", + "method": "GET" + }, + { + "path": "/v3/services", + "method": "GET" + }, + { + "path": "/v3/services", + "method": "POST" + }, + { + "path": "/v3/services/*", + "method": "GET" + }, + { + "path": "/v3/services/*", + "method": "PATCH" + }, + { + "path": "/v3/services/*", + "method": "DELETE" + }, + { + "path": "/v3/endpoints", + "method": "GET" + }, + { + "path": "/v3/endpoints", + "method": "POST" + }, + { + "path": "/v3/endpoints/*", + "method": "GET" + }, + { + "path": "/v3/endpoints/*", + "method": "PATCH" + }, + { + "path": "/v3/endpoints/*", + "method": "DELETE" + }, + { + "path": "/v3/system/users/*/roles", + "method": "GET" + }, + { + "path": "/v3/system/users/*/roles/*", + "method": "PUT" + }, + { + "path": "/v3/system/users/*/roles/*", + "method": "HEAD" + }, + { + "path": "/v3/system/users/*/roles/*", + "method": "GET" + }, + { + "path": "/v3/system/users/*/roles/*", + "method": "DELETE" + }, + { + "path": "/v3/system/groups/*/roles", + "method": "GET" + }, + { + "path": "/v3/system/groups/*/roles/*", + "method": "PUT" + }, + { + "path": "/v3/system/groups/*/roles/*", + "method": "HEAD" + }, + { + "path": "/v3/system/groups/*/roles/*", + "method": "GET" + }, + { + "path": "/v3/system/groups/*/roles/*", + "method": "DELETE" + }, + { + "path": "/v3/registered_limits", + "method": "GET" + }, + { + "path": "/v3/registered_limits", + "method": "POST" + }, + { + "path": "/v3/registered_limits/*", + "method": "PATCH" + }, + { + "path": "/v3/registered_limits/*", + "method": "GET" + }, + { + "path": "/v3/registered_limits/*", + "method": "DELETE" + }, + { + "path": "/v3/limits/model", + "method": "GET" + }, + { + "path": "/v3/limits", + "method": "GET" + }, + { + "path": "/v3/limits", + "method": "POST" + }, + { + "path": "/v3/limits/*", + "method": "PATCH" + }, + { + "path": "/v3/limits/*", + "method": "GET" + }, + { + "path": "/v3/limits/*", + "method": "DELETE" + }, + { + "path": "/v3/users", + "method": "GET" + }, + { + "path": "/v3/users", + "method": "POST" + }, + { + "path": "/v3/users/*", + "method": "GET" + }, + { + "path": "/v3/users/*", + "method": "PATCH" + }, + { + "path": "/v3/users/*", + "method": "DELETE" + }, + { + "path": "/v3/users/*/groups", + "method": "GET" + }, + { + "path": "/v3/users/*/projects", + "method": "GET" + }, + { + "path": "/v3/users/*/password", + "method": "POST" + } + ], + "image": [ + { + "path": "/v1/images", + "method": "POST" + }, + { + "path": "/v1/images", + "method": "GET" + }, + { + "path": "/v1/images/detail", + "method": "GET" + }, + { + "path": "/v1/images/*", + "method": "PUT" + }, + { + "path": "/v1/images/*", + "method": "GET" + }, + { + "path": "/v1/images/*", + "method": "HEAD" + }, + { + "path": "/v1/images/*", + "method": "DELETE" + }, + { + "path": "/v1/images/*/members/*", + "method": "PUT" + }, + { + "path": "/v1/images/*/members", + "method": "PUT" + }, + { + "path": "/v1/images/*/members/*", + "method": "DELETE" + }, + { + "path": "/v1/shared-images/*", + "method": "GET" + }, + { + "path": "/v2/images/*/file", + "method": "PUT" + }, + { + "path": "/v2/images/*/file", + "method": "GET" + }, + { + "path": "/v2/images", + "method": "POST" + }, + { + "path": "/v2/images/*", + "method": "GET" + }, + { + "path": "/v2/images", + "method": "GET" + }, + { + "path": "/v2/images/*", + "method": "PATCH" + }, + { + "path": "/v2/images/*", + "method": "DELETE" + }, + { + "path": "/v2/images/*/actions/deactivate", + "method": "POST" + }, + { + "path": "/v2/images/*/actions/reactivate", + "method": "POST" + }, + { + "path": "/v2/schemas/images", + "method": "GET" + }, + { + "path": "/v2/schemas/image", + "method": "GET" + }, + { + "path": "/v2/schemas/members", + "method": "GET" + }, + { + "path": "/v2/schemas/member", + "method": "GET" + }, + { + "path": "/v2/images/*/members", + "method": "POST" + }, + { + "path": "/v2/images/*/members/*", + "method": "GET" + }, + { + "path": "/v2/images/*/members", + "method": "GET" + }, + { + "path": "/v2/images/*/members/*", + "method": "PUT" + }, + { + "path": "/v2/images/*/members/*", + "method": "DELETE" + }, + { + "path": "/v2/images/*/tags/*", + "method": "PUT" + }, + { + "path": "/v2/images/*/tags/*", + "method": "DELETE" + }, + { + "path": "/v2/metadefs/namespaces/*/objects", + "method": "POST" + }, + { + "path": "/v2/metadefs/namespaces/*/objects", + "method": "GET" + }, + { + "path": "/v2/metadefs/namespaces/*/objects/*", + "method": "GET" + }, + { + "path": "/v2/metadefs/namespaces/*/objects/*", + "method": "PUT" + }, + { + "path": "/v2/metadefs/namespaces/*/objects/*", + "method": "DELETE" + }, + { + "path": "/v2/metadefs/namespaces/*/properties", + "method": "POST" + }, + { + "path": "/v2/metadefs/namespaces/*/properties", + "method": "GET" + }, + { + "path": "/v2/metadefs/namespaces/*/properties/*", + "method": "GET" + }, + { + "path": "/v2/metadefs/namespaces/*/properties/*", + "method": "PUT" + }, + { + "path": "/v2/metadefs/namespaces/*/properties/*", + "method": "DELETE" + }, + { + "path": "/v2/metadefs/namespaces/*/tags/*", + "method": "POST" + }, + { + "path": "/v2/metadefs/namespaces/*/tags/*", + "method": "GET" + }, + { + "path": "/v2/metadefs/namespaces/*/tags/*", + "method": "PUT" + }, + { + "path": "/v2/metadefs/namespaces/*/tags/*", + "method": "DELETE" + }, + { + "path": "/v2/metadefs/namespaces/*/tags", + "method": "POST" + }, + { + "path": "/v2/metadefs/namespaces/*/tags", + "method": "GET" + }, + { + "path": "/v2/metadefs/namespaces/*/tags", + "method": "DELETE" + }, + { + "path": "/v2/metadefs/namespaces", + "method": "POST" + }, + { + "path": "/v2/metadefs/namespaces", + "method": "GET" + }, + { + "path": "/v2/metadefs/namespaces/*", + "method": "GET" + }, + { + "path": "/v2/metadefs/namespaces/*", + "method": "PUT" + }, + { + "path": "/v2/metadefs/namespaces/*", + "method": "DELETE" + }, + { + "path": "/v2/metadefs/resource_types", + "method": "GET" + }, + { + "path": "/v2/metadefs/namespaces/*/resource_types", + "method": "POST" + }, + { + "path": "/v2/metadefs/namespaces/*/resource_types", + "method": "GET" + }, + { + "path": "/v2/metadefs/namespaces/*/resource_types/*", + "method": "DELETE" + }, + { + "path": "/v2/schemas/metadefs/namespace", + "method": "GET" + }, + { + "path": "/v2/schemas/metadefs/namespaces", + "method": "GET" + }, + { + "path": "/v2/schemas/metadefs/resource_type", + "method": "GET" + }, + { + "path": "/v2/schemas/metadefs/resource_types", + "method": "GET" + }, + { + "path": "/v2/schemas/metadefs/object", + "method": "GET" + }, + { + "path": "/v2/schemas/metadefs/objects", + "method": "GET" + }, + { + "path": "/v2/schemas/metadefs/property", + "method": "GET" + }, + { + "path": "/v2/schemas/metadefs/properties", + "method": "GET" + }, + { + "path": "/v2/schemas/metadefs/tag", + "method": "GET" + }, + { + "path": "/v2/schemas/metadefs/tags", + "method": "GET" + }, + { + "path": "/v2/schemas/tasks", + "method": "GET" + }, + { + "path": "/v2/schemas/task", + "method": "GET" + }, + { + "path": "/v2/tasks", + "method": "POST" + }, + { + "path": "/v2/tasks", + "method": "GET" + }, + { + "path": "/v2/tasks/*", + "method": "GET" + }, + { + "path": "/versions", + "method": "GET" + }, + { + "path": "/", + "method": "GET" + } + ], + "block-storage": [ + { + "path": "/v3/**", + "method": "GET" + } + ], + "compute": [ + { + "path": "**", + "method": "GET" + } + ] +} diff --git a/keystone/tests/unit/ksfixtures/__init__.py b/keystone/tests/unit/ksfixtures/__init__.py index 7a92c42cd0..2203e50891 100644 --- a/keystone/tests/unit/ksfixtures/__init__.py +++ b/keystone/tests/unit/ksfixtures/__init__.py @@ -11,6 +11,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +from keystone.tests.unit.ksfixtures.access_rules_config import AccessRulesConfig # noqa from keystone.tests.unit.ksfixtures.auth_plugins import ConfigAuthPlugins # noqa from keystone.tests.unit.ksfixtures.backendloader import BackendLoader # noqa from keystone.tests.unit.ksfixtures.cache import Cache # noqa diff --git a/keystone/tests/unit/ksfixtures/access_rules_config.py b/keystone/tests/unit/ksfixtures/access_rules_config.py new file mode 100644 index 0000000000..1efffbb274 --- /dev/null +++ b/keystone/tests/unit/ksfixtures/access_rules_config.py @@ -0,0 +1,29 @@ +# Copyright 2019 SUSE Linux GmbH +# +# 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 fixtures + + +class AccessRulesConfig(fixtures.Fixture): + """A fixture for working with JSON access rules config.""" + + def __init__(self, config_fixture, rules_file=None): + self._config_fixture = config_fixture + self._rules_file = rules_file + + def setUp(self): + super(AccessRulesConfig, self).setUp() + self._config_fixture.config(group='access_rules_config', + rules_file=self._rules_file) diff --git a/setup.cfg b/setup.cfg index 47a9469ad9..20f674d9ac 100644 --- a/setup.cfg +++ b/setup.cfg @@ -178,6 +178,9 @@ keystone.unified_limit.model = flat = keystone.limit.models.flat:FlatModel strict_two_level = keystone.limit.models.strict_two_level:StrictTwoLevelModel +keystone.access_rules_config = + json = keystone.access_rules_config.backends.json:AccessRulesConfig + oslo.config.opts = keystone = keystone.conf.opts:list_opts