diff --git a/api-ref/source/v3/domains-config-v3.inc b/api-ref/source/v3/domains-config-v3.inc index 03abcea9ea..e0db65808f 100644 --- a/api-ref/source/v3/domains-config-v3.inc +++ b/api-ref/source/v3/domains-config-v3.inc @@ -42,7 +42,7 @@ Show default configuration settings The default configuration settings for the options that can be overridden can be retrieved. -Relationship:: +Relationship: ``https://docs.openstack.org/api/openstack-identity/3/rel/domain_config_default`` Response Parameters @@ -145,7 +145,6 @@ Response Example .. literalinclude:: ./samples/admin/domain-config-group-option-default-response.json :language: javascript - Show domain group option configuration ====================================== diff --git a/keystone/resource/routers.py b/keystone/resource/routers.py index ca6bf38e8d..7e8936bf30 100644 --- a/keystone/resource/routers.py +++ b/keystone/resource/routers.py @@ -88,13 +88,13 @@ class Routers(wsgi.RoutersBase): self._add_resource( mapper, config_controller, path='/domains/config/default', - get_action='get_domain_config_default', + get_head_action='get_domain_config_default', rel=json_home.build_v3_resource_relation('domain_config_default')) self._add_resource( mapper, config_controller, path='/domains/config/{group}/default', - get_action='get_domain_config_default', + get_head_action='get_domain_config_default', rel=json_home.build_v3_resource_relation( 'domain_config_default_group'), path_vars={ @@ -104,7 +104,7 @@ class Routers(wsgi.RoutersBase): self._add_resource( mapper, config_controller, path='/domains/config/{group}/{option}/default', - get_action='get_domain_config_default', + get_head_action='get_domain_config_default', rel=json_home.build_v3_resource_relation( 'domain_config_default_option'), path_vars={ diff --git a/keystone/tests/unit/test_v3_domain_config.py b/keystone/tests/unit/test_v3_domain_config.py index 598e828f9a..38fcc38e4c 100644 --- a/keystone/tests/unit/test_v3_domain_config.py +++ b/keystone/tests/unit/test_v3_domain_config.py @@ -122,7 +122,7 @@ class DomainConfigTestCase(test_v3.RestfulTestCase): self.assertEqual(self.config, r.result['config']) self.head(url, expected_status=http_client.OK) - def test_get_config_by_group(self): + def test_get_head_config_by_group(self): """Call ``GET & HEAD /domains{domain_id}/config/{group}``.""" self.domain_config_api.create_config(self.domain['id'], self.config) url = '/domains/%(domain_id)s/config/ldap' % { @@ -131,7 +131,7 @@ class DomainConfigTestCase(test_v3.RestfulTestCase): self.assertEqual({'ldap': self.config['ldap']}, r.result['config']) self.head(url, expected_status=http_client.OK) - def test_get_config_by_group_invalid_domain(self): + def test_get_head_config_by_group_invalid_domain(self): """Call ``GET & HEAD /domains{domain_id}/config/{group}``. While retrieving Identity API-based domain config by group with an @@ -140,11 +140,13 @@ class DomainConfigTestCase(test_v3.RestfulTestCase): """ self.domain_config_api.create_config(self.domain['id'], self.config) invalid_domain_id = uuid.uuid4().hex - self.get('/domains/%(domain_id)s/config/ldap' % { - 'domain_id': invalid_domain_id}, - expected_status=exception.DomainNotFound.code) + url = ('/domains/%(domain_id)s/config/ldap' % { + 'domain_id': invalid_domain_id} + ) + self.get(url, expected_status=exception.DomainNotFound.code) + self.head(url, expected_status=exception.DomainNotFound.code) - def test_get_config_by_option(self): + def test_get_head_config_by_option(self): """Call ``GET & HEAD /domains{domain_id}/config/{group}/{option}``.""" self.domain_config_api.create_config(self.domain['id'], self.config) url = '/domains/%(domain_id)s/config/ldap/url' % { @@ -154,7 +156,7 @@ class DomainConfigTestCase(test_v3.RestfulTestCase): r.result['config']) self.head(url, expected_status=http_client.OK) - def test_get_config_by_option_invalid_domain(self): + def test_get_head_config_by_option_invalid_domain(self): """Call ``GET & HEAD /domains{domain_id}/config/{group}/{option}``. While retrieving Identity API-based domain config by option with an @@ -163,38 +165,46 @@ class DomainConfigTestCase(test_v3.RestfulTestCase): """ self.domain_config_api.create_config(self.domain['id'], self.config) invalid_domain_id = uuid.uuid4().hex - self.get('/domains/%(domain_id)s/config/ldap/url' % { - 'domain_id': invalid_domain_id}, - expected_status=exception.DomainNotFound.code) + url = ('/domains/%(domain_id)s/config/ldap/url' % { + 'domain_id': invalid_domain_id} + ) + self.get(url, expected_status=exception.DomainNotFound.code) + self.head(url, expected_status=exception.DomainNotFound.code) - def test_get_non_existant_config(self): + def test_get_head_non_existant_config(self): """Call ``GET /domains{domain_id}/config when no config defined``.""" - self.get('/domains/%(domain_id)s/config' % { - 'domain_id': self.domain['id']}, - expected_status=http_client.NOT_FOUND) + url = ('/domains/%(domain_id)s/config' % { + 'domain_id': self.domain['id']} + ) + self.get(url, expected_status=http_client.NOT_FOUND) + self.head(url, expected_status=http_client.NOT_FOUND) - def test_get_non_existant_config_invalid_domain(self): - """Call ``GET /domains{domain_id}/config when no config defined``. + def test_get_head_non_existant_config_invalid_domain(self): + """Call ``GET & HEAD /domains/{domain_id}/config with invalid domain``. While retrieving non-existent Identity API-based domain config with an invalid domain id provided, the request shall be rejected with a response 404 domain not found. """ invalid_domain_id = uuid.uuid4().hex - self.get('/domains/%(domain_id)s/config' % { - 'domain_id': invalid_domain_id}, - expected_status=exception.DomainNotFound.code) + url = ('/domains/%(domain_id)s/config' % { + 'domain_id': invalid_domain_id} + ) + self.get(url, expected_status=exception.DomainNotFound.code) + self.head(url, expected_status=exception.DomainNotFound.code) - def test_get_non_existant_config_group(self): - """Call ``GET /domains{domain_id}/config/{group_not_exist}``.""" + def test_get_head_non_existant_config_group(self): + """Call ``GET /domains/{domain_id}/config/{group_not_exist}``.""" config = {'ldap': {'url': uuid.uuid4().hex}} self.domain_config_api.create_config(self.domain['id'], config) - self.get('/domains/%(domain_id)s/config/identity' % { - 'domain_id': self.domain['id']}, - expected_status=http_client.NOT_FOUND) + url = ('/domains/%(domain_id)s/config/identity' % { + 'domain_id': self.domain['id']} + ) + self.get(url, expected_status=http_client.NOT_FOUND) + self.head(url, expected_status=http_client.NOT_FOUND) - def test_get_non_existant_config_group_invalid_domain(self): - """Call ``GET /domains{domain_id}/config/{group_not_exist}``. + def test_get_head_non_existant_config_group_invalid_domain(self): + """Call ``GET & HEAD /domains/{domain_id}/config/{group}``. While retrieving non-existent Identity API-based domain config group with an invalid domain id provided, the request shall be rejected with @@ -203,20 +213,31 @@ class DomainConfigTestCase(test_v3.RestfulTestCase): config = {'ldap': {'url': uuid.uuid4().hex}} self.domain_config_api.create_config(self.domain['id'], config) invalid_domain_id = uuid.uuid4().hex - self.get('/domains/%(domain_id)s/config/identity' % { - 'domain_id': invalid_domain_id}, - expected_status=exception.DomainNotFound.code) + url = ('/domains/%(domain_id)s/config/identity' % { + 'domain_id': invalid_domain_id} + ) + self.get(url, expected_status=exception.DomainNotFound.code) + self.head(url, expected_status=exception.DomainNotFound.code) - def test_get_non_existant_config_option(self): - """Call ``GET /domains{domain_id}/config/group/{option_not_exist}``.""" + def test_get_head_non_existant_config_option(self): + """Test that Not Found is returned when option doesn't exist. + + Call ``GET & HEAD /domains/{domain_id}/config/{group}/{opt_not_exist}`` + and ensure a Not Found is returned because the option isn't defined + within the group. + """ config = {'ldap': {'url': uuid.uuid4().hex}} self.domain_config_api.create_config(self.domain['id'], config) - self.get('/domains/%(domain_id)s/config/ldap/user_tree_dn' % { - 'domain_id': self.domain['id']}, - expected_status=http_client.NOT_FOUND) + url = ('/domains/%(domain_id)s/config/ldap/user_tree_dn' % { + 'domain_id': self.domain['id']} + ) + self.get(url, expected_status=http_client.NOT_FOUND) + self.head(url, expected_status=http_client.NOT_FOUND) - def test_get_non_existant_config_option_invalid_domain(self): - """Call ``GET /domains{domain_id}/config/group/{option_not_exist}``. + def test_get_head_non_existant_config_option_with_invalid_domain(self): + """Test that Domain Not Found is returned with invalid domain. + + Call ``GET & HEAD /domains/{domain_id}/config/{group}/{opt_not_exist}`` While retrieving non-existent Identity API-based domain config option with an invalid domain id provided, the request shall be rejected with @@ -225,9 +246,11 @@ class DomainConfigTestCase(test_v3.RestfulTestCase): config = {'ldap': {'url': uuid.uuid4().hex}} self.domain_config_api.create_config(self.domain['id'], config) invalid_domain_id = uuid.uuid4().hex - self.get('/domains/%(domain_id)s/config/ldap/user_tree_dn' % { - 'domain_id': invalid_domain_id}, - expected_status=exception.DomainNotFound.code) + url = ('/domains/%(domain_id)s/config/ldap/user_tree_dn' % { + 'domain_id': invalid_domain_id} + ) + self.get(url, expected_status=exception.DomainNotFound.code) + self.head(url, expected_status=exception.DomainNotFound.code) def test_update_config(self): """Call ``PATCH /domains/{domain_id}/config``.""" @@ -402,8 +425,8 @@ class DomainConfigTestCase(test_v3.RestfulTestCase): body={'config': new_config}, expected_status=exception.DomainNotFound.code) - def test_get_config_default(self): - """Call ``GET /domains/config/default``.""" + def test_get_head_config_default(self): + """Call ``GET & HEAD /domains/config/default``.""" # Create a config that overrides a few of the options so that we can # check that only the defaults are returned. self.domain_config_api.create_config(self.domain['id'], self.config) @@ -414,9 +437,10 @@ class DomainConfigTestCase(test_v3.RestfulTestCase): for option in default_config[group]: self.assertEqual(getattr(getattr(CONF, group), option), default_config[group][option]) + self.head(url, expected_status=http_client.OK) - def test_get_config_default_by_group(self): - """Call ``GET /domains/config/{group}/default``.""" + def test_get_head_config_default_by_group(self): + """Call ``GET & HEAD /domains/config/{group}/default``.""" # Create a config that overrides a few of the options so that we can # check that only the defaults are returned. self.domain_config_api.create_config(self.domain['id'], self.config) @@ -426,9 +450,10 @@ class DomainConfigTestCase(test_v3.RestfulTestCase): for option in default_config['ldap']: self.assertEqual(getattr(CONF.ldap, option), default_config['ldap'][option]) + self.head(url, expected_status=http_client.OK) - def test_get_config_default_by_option(self): - """Call ``GET /domains/config/{group}/{option}/default``.""" + def test_get_head_config_default_by_option(self): + """Call ``GET & HEAD /domains/config/{group}/{option}/default``.""" # Create a config that overrides a few of the options so that we can # check that only the defaults are returned. self.domain_config_api.create_config(self.domain['id'], self.config) @@ -436,27 +461,36 @@ class DomainConfigTestCase(test_v3.RestfulTestCase): r = self.get(url) default_config = r.result['config'] self.assertEqual(CONF.ldap.url, default_config['url']) + self.head(url, expected_status=http_client.OK) - def test_get_config_default_by_invalid_group(self): - """Call ``GET for /domains/config/{bad-group}/default``.""" + def test_get_head_config_default_by_invalid_group(self): + """Call ``GET & HEAD for /domains/config/{bad-group}/default``.""" # First try a valid group, but one we don't support for domain config - self.get('/domains/config/resouce/default', + self.get('/domains/config/resource/default', expected_status=http_client.FORBIDDEN) + self.head('/domains/config/resource/default', + expected_status=http_client.FORBIDDEN) # Now try a totally invalid group url = '/domains/config/%s/default' % uuid.uuid4().hex self.get(url, expected_status=http_client.FORBIDDEN) + self.head(url, expected_status=http_client.FORBIDDEN) - def test_get_config_default_by_invalid_option(self): - """Call ``GET for /domains/config/{group}/{bad-option}/default``.""" - # First try a valid option, but one we don't support for domain config, - # i.e. one that is in the sensitive options list + def test_get_head_config_default_for_unsupported_group(self): + # It should not be possible to expose configuration information for + # groups that the domain configuration API backlists explicitly. Doing + # so would be a security vulnerability because it would leak sensitive + # information over the API. self.get('/domains/config/ldap/password/default', expected_status=http_client.FORBIDDEN) + self.head('/domains/config/ldap/password/default', + expected_status=http_client.FORBIDDEN) - # Now try a totally invalid option + def test_get_head_config_default_for_invalid_option(self): + """Returning invalid configuration options is invalid.""" url = '/domains/config/ldap/%s/default' % uuid.uuid4().hex self.get(url, expected_status=http_client.FORBIDDEN) + self.head(url, expected_status=http_client.FORBIDDEN) class SecurityRequirementsTestCase(test_v3.RestfulTestCase): @@ -518,7 +552,7 @@ class SecurityRequirementsTestCase(test_v3.RestfulTestCase): ) return self.get_requested_token(non_admin_auth_data) - def test_get_security_compliance_config_for_default_domain(self): + def test_get_head_security_compliance_config_for_default_domain(self): """Ask for all security compliance configuration options. Support for enforcing security compliance per domain currently doesn't @@ -557,6 +591,18 @@ class SecurityRequirementsTestCase(test_v3.RestfulTestCase): admin_response = self.get(url, token=self._get_admin_token()) self.assertEqual(admin_response.result['config'], expected_response) + # Ensure HEAD requests behave the same way + self.head( + url, + token=self._get_non_admin_token(), + expected_status=http_client.OK + ) + self.head( + url, + token=self._get_admin_token(), + expected_status=http_client.OK + ) + def test_get_security_compliance_config_for_non_default_domain_fails(self): """Getting security compliance opts for other domains should fail. @@ -600,6 +646,18 @@ class SecurityRequirementsTestCase(test_v3.RestfulTestCase): token=self._get_admin_token() ) + # Ensure HEAD requests behave the same way + self.head( + url, + expected_status=http_client.FORBIDDEN, + token=self._get_non_admin_token() + ) + self.head( + url, + expected_status=http_client.FORBIDDEN, + token=self._get_admin_token() + ) + def test_get_non_whitelisted_security_compliance_opt_fails(self): """We only support exposing a subset of security compliance options. @@ -634,6 +692,18 @@ class SecurityRequirementsTestCase(test_v3.RestfulTestCase): token=self._get_admin_token() ) + # Ensure HEAD requests behave the same way + self.head( + url, + expected_status=http_client.FORBIDDEN, + token=self._get_non_admin_token() + ) + self.head( + url, + expected_status=http_client.FORBIDDEN, + token=self._get_admin_token() + ) + def test_get_security_compliance_password_regex(self): """Ask for the security compliance password regular expression.""" password_regex = uuid.uuid4().hex @@ -665,6 +735,18 @@ class SecurityRequirementsTestCase(test_v3.RestfulTestCase): password_regex ) + # Ensure HEAD requests behave the same way + self.head( + url, + token=self._get_non_admin_token(), + expected_status=http_client.OK + ) + self.head( + url, + token=self._get_admin_token(), + expected_status=http_client.OK + ) + def test_get_security_compliance_password_regex_description(self): """Ask for the security compliance password regex description.""" password_regex_description = uuid.uuid4().hex @@ -696,6 +778,18 @@ class SecurityRequirementsTestCase(test_v3.RestfulTestCase): password_regex_description ) + # Ensure HEAD requests behave the same way + self.head( + url, + token=self._get_non_admin_token(), + expected_status=http_client.OK + ) + self.head( + url, + token=self._get_admin_token(), + expected_status=http_client.OK + ) + def test_get_security_compliance_password_regex_returns_none(self): """When an option isn't set, we should explicitly return None.""" group = 'security_compliance' @@ -717,6 +811,18 @@ class SecurityRequirementsTestCase(test_v3.RestfulTestCase): admin_response = self.get(url, token=self._get_admin_token()) self.assertIsNone(admin_response.result['config'][option]) + # Ensure HEAD requests behave the same way + self.head( + url, + token=self._get_non_admin_token(), + expected_status=http_client.OK + ) + self.head( + url, + token=self._get_admin_token(), + expected_status=http_client.OK + ) + def test_get_security_compliance_password_regex_desc_returns_none(self): """When an option isn't set, we should explicitly return None.""" group = 'security_compliance' @@ -738,6 +844,18 @@ class SecurityRequirementsTestCase(test_v3.RestfulTestCase): admin_response = self.get(url, token=self._get_admin_token()) self.assertIsNone(admin_response.result['config'][option]) + # Ensure HEAD requests behave the same way + self.head( + url, + token=self._get_non_admin_token(), + expected_status=http_client.OK + ) + self.head( + url, + token=self._get_admin_token(), + expected_status=http_client.OK + ) + def test_get_security_compliance_config_with_user_from_other_domain(self): """Make sure users from other domains can access password requirements. @@ -807,6 +925,13 @@ class SecurityRequirementsTestCase(test_v3.RestfulTestCase): password_regex_description ) + # Ensure HEAD requests behave the same way + self.head( + url, + token=user_token, + expected_status=http_client.OK + ) + def test_update_security_compliance_config_group_fails(self): """Make sure that updates to the entire security group section fail.