keystone/keystone/tests/unit/test_v3_domain_config.py

1169 lines
46 KiB
Python

# 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 copy
import uuid
from six.moves import http_client
import keystone.conf
from keystone import exception
from keystone.tests import unit
from keystone.tests.unit import test_v3
CONF = keystone.conf.CONF
class DomainConfigTestCase(test_v3.RestfulTestCase):
"""Test domain config support."""
def setUp(self):
super(DomainConfigTestCase, self).setUp()
self.domain = unit.new_domain_ref()
self.resource_api.create_domain(self.domain['id'], self.domain)
self.config = {'ldap': {'url': uuid.uuid4().hex,
'user_tree_dn': uuid.uuid4().hex},
'identity': {'driver': uuid.uuid4().hex}}
def test_create_config(self):
"""Call ``PUT /domains/{domain_id}/config``."""
url = '/domains/%(domain_id)s/config' % {
'domain_id': self.domain['id']}
r = self.put(url, body={'config': self.config},
expected_status=http_client.CREATED)
res = self.domain_config_api.get_config(self.domain['id'])
self.assertEqual(self.config, r.result['config'])
self.assertEqual(self.config, res)
def test_create_config_invalid_domain(self):
"""Call ``PUT /domains/{domain_id}/config``.
While creating 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
url = '/domains/%(domain_id)s/config' % {
'domain_id': invalid_domain_id}
self.put(url, body={'config': self.config},
expected_status=exception.DomainNotFound.code)
def test_create_config_twice(self):
"""Check multiple creates don't throw error."""
self.put('/domains/%(domain_id)s/config' % {
'domain_id': self.domain['id']},
body={'config': self.config},
expected_status=http_client.CREATED)
self.put('/domains/%(domain_id)s/config' % {
'domain_id': self.domain['id']},
body={'config': self.config},
expected_status=http_client.OK)
def test_delete_config(self):
"""Call ``DELETE /domains{domain_id}/config``."""
self.domain_config_api.create_config(self.domain['id'], self.config)
self.delete('/domains/%(domain_id)s/config' % {
'domain_id': self.domain['id']})
self.get('/domains/%(domain_id)s/config' % {
'domain_id': self.domain['id']},
expected_status=exception.DomainConfigNotFound.code)
def test_delete_config_invalid_domain(self):
"""Call ``DELETE /domains{domain_id}/config``.
While deleting Identity API-based domain config with an invalid domain
id provided, the request shall be rejected with a response, 404 domain
not found.
"""
self.domain_config_api.create_config(self.domain['id'], self.config)
invalid_domain_id = uuid.uuid4().hex
self.delete('/domains/%(domain_id)s/config' % {
'domain_id': invalid_domain_id},
expected_status=exception.DomainNotFound.code)
def test_delete_config_by_group(self):
"""Call ``DELETE /domains{domain_id}/config/{group}``."""
self.domain_config_api.create_config(self.domain['id'], self.config)
self.delete('/domains/%(domain_id)s/config/ldap' % {
'domain_id': self.domain['id']})
res = self.domain_config_api.get_config(self.domain['id'])
self.assertNotIn('ldap', res)
def test_delete_config_by_group_invalid_domain(self):
"""Call ``DELETE /domains{domain_id}/config/{group}``.
While deleting Identity API-based domain config by group with an
invalid domain id provided, the request shall be rejected with a
response 404 domain not found.
"""
self.domain_config_api.create_config(self.domain['id'], self.config)
invalid_domain_id = uuid.uuid4().hex
self.delete('/domains/%(domain_id)s/config/ldap' % {
'domain_id': invalid_domain_id},
expected_status=exception.DomainNotFound.code)
def test_get_head_config(self):
"""Call ``GET & HEAD for /domains{domain_id}/config``."""
self.domain_config_api.create_config(self.domain['id'], self.config)
url = '/domains/%(domain_id)s/config' % {
'domain_id': self.domain['id']}
r = self.get(url)
self.assertEqual(self.config, r.result['config'])
self.head(url, expected_status=http_client.OK)
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' % {
'domain_id': self.domain['id']}
r = self.get(url)
self.assertEqual({'ldap': self.config['ldap']}, r.result['config'])
self.head(url, expected_status=http_client.OK)
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
invalid domain id provided, the request shall be rejected with a
response 404 domain not found.
"""
self.domain_config_api.create_config(self.domain['id'], self.config)
invalid_domain_id = uuid.uuid4().hex
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_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' % {
'domain_id': self.domain['id']}
r = self.get(url)
self.assertEqual({'url': self.config['ldap']['url']},
r.result['config'])
self.head(url, expected_status=http_client.OK)
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
invalid domain id provided, the request shall be rejected with a
response 404 domain not found.
"""
self.domain_config_api.create_config(self.domain['id'], self.config)
invalid_domain_id = uuid.uuid4().hex
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_head_non_existant_config(self):
"""Call ``GET /domains{domain_id}/config when no config defined``."""
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_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
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_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)
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_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
a response, 404 domain not found.
"""
config = {'ldap': {'url': uuid.uuid4().hex}}
self.domain_config_api.create_config(self.domain['id'], config)
invalid_domain_id = uuid.uuid4().hex
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_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)
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_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
a response, 404 domain not found.
"""
config = {'ldap': {'url': uuid.uuid4().hex}}
self.domain_config_api.create_config(self.domain['id'], config)
invalid_domain_id = uuid.uuid4().hex
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``."""
self.domain_config_api.create_config(self.domain['id'], self.config)
new_config = {'ldap': {'url': uuid.uuid4().hex},
'identity': {'driver': uuid.uuid4().hex}}
r = self.patch('/domains/%(domain_id)s/config' % {
'domain_id': self.domain['id']},
body={'config': new_config})
res = self.domain_config_api.get_config(self.domain['id'])
expected_config = copy.deepcopy(self.config)
expected_config['ldap']['url'] = new_config['ldap']['url']
expected_config['identity']['driver'] = (
new_config['identity']['driver'])
self.assertEqual(expected_config, r.result['config'])
self.assertEqual(expected_config, res)
def test_update_config_invalid_domain(self):
"""Call ``PATCH /domains/{domain_id}/config``.
While updating Identity API-based domain config with an invalid domain
id provided, the request shall be rejected with a response, 404 domain
not found.
"""
self.domain_config_api.create_config(self.domain['id'], self.config)
new_config = {'ldap': {'url': uuid.uuid4().hex},
'identity': {'driver': uuid.uuid4().hex}}
invalid_domain_id = uuid.uuid4().hex
self.patch('/domains/%(domain_id)s/config' % {
'domain_id': invalid_domain_id},
body={'config': new_config},
expected_status=exception.DomainNotFound.code)
def test_update_config_group(self):
"""Call ``PATCH /domains/{domain_id}/config/{group}``."""
self.domain_config_api.create_config(self.domain['id'], self.config)
new_config = {'ldap': {'url': uuid.uuid4().hex,
'user_filter': uuid.uuid4().hex}}
r = self.patch('/domains/%(domain_id)s/config/ldap' % {
'domain_id': self.domain['id']},
body={'config': new_config})
res = self.domain_config_api.get_config(self.domain['id'])
expected_config = copy.deepcopy(self.config)
expected_config['ldap']['url'] = new_config['ldap']['url']
expected_config['ldap']['user_filter'] = (
new_config['ldap']['user_filter'])
self.assertEqual(expected_config, r.result['config'])
self.assertEqual(expected_config, res)
def test_update_config_group_invalid_domain(self):
"""Call ``PATCH /domains/{domain_id}/config/{group}``.
While updating Identity API-based domain config group with an invalid
domain id provided, the request shall be rejected with a response,
404 domain not found.
"""
self.domain_config_api.create_config(self.domain['id'], self.config)
new_config = {'ldap': {'url': uuid.uuid4().hex,
'user_filter': uuid.uuid4().hex}}
invalid_domain_id = uuid.uuid4().hex
self.patch('/domains/%(domain_id)s/config/ldap' % {
'domain_id': invalid_domain_id},
body={'config': new_config},
expected_status=exception.DomainNotFound.code)
def test_update_config_invalid_group(self):
"""Call ``PATCH /domains/{domain_id}/config/{invalid_group}``."""
self.domain_config_api.create_config(self.domain['id'], self.config)
# Trying to update a group that is neither whitelisted or sensitive
# should result in Forbidden.
invalid_group = uuid.uuid4().hex
new_config = {invalid_group: {'url': uuid.uuid4().hex,
'user_filter': uuid.uuid4().hex}}
self.patch('/domains/%(domain_id)s/config/%(invalid_group)s' % {
'domain_id': self.domain['id'], 'invalid_group': invalid_group},
body={'config': new_config},
expected_status=http_client.FORBIDDEN)
# Trying to update a valid group, but one that is not in the current
# config should result in NotFound
config = {'ldap': {'suffix': uuid.uuid4().hex}}
self.domain_config_api.create_config(self.domain['id'], config)
new_config = {'identity': {'driver': uuid.uuid4().hex}}
self.patch('/domains/%(domain_id)s/config/identity' % {
'domain_id': self.domain['id']},
body={'config': new_config},
expected_status=http_client.NOT_FOUND)
def test_update_config_invalid_group_invalid_domain(self):
"""Call ``PATCH /domains/{domain_id}/config/{invalid_group}``.
While updating Identity API-based domain config with an invalid group
and an invalid domain id provided, the request shall be rejected
with a response, 404 domain not found.
"""
self.domain_config_api.create_config(self.domain['id'], self.config)
invalid_group = uuid.uuid4().hex
new_config = {invalid_group: {'url': uuid.uuid4().hex,
'user_filter': uuid.uuid4().hex}}
invalid_domain_id = uuid.uuid4().hex
self.patch('/domains/%(domain_id)s/config/%(invalid_group)s' % {
'domain_id': invalid_domain_id,
'invalid_group': invalid_group},
body={'config': new_config},
expected_status=exception.DomainNotFound.code)
def test_update_config_option(self):
"""Call ``PATCH /domains/{domain_id}/config/{group}/{option}``."""
self.domain_config_api.create_config(self.domain['id'], self.config)
new_config = {'url': uuid.uuid4().hex}
r = self.patch('/domains/%(domain_id)s/config/ldap/url' % {
'domain_id': self.domain['id']},
body={'config': new_config})
res = self.domain_config_api.get_config(self.domain['id'])
expected_config = copy.deepcopy(self.config)
expected_config['ldap']['url'] = new_config['url']
self.assertEqual(expected_config, r.result['config'])
self.assertEqual(expected_config, res)
def test_update_config_option_invalid_domain(self):
"""Call ``PATCH /domains/{domain_id}/config/{group}/{option}``.
While updating Identity API-based domain config option with an invalid
domain id provided, the request shall be rejected with a response, 404
domain not found.
"""
self.domain_config_api.create_config(self.domain['id'], self.config)
new_config = {'url': uuid.uuid4().hex}
invalid_domain_id = uuid.uuid4().hex
self.patch('/domains/%(domain_id)s/config/ldap/url' % {
'domain_id': invalid_domain_id},
body={'config': new_config},
expected_status=exception.DomainNotFound.code)
def test_update_config_invalid_option(self):
"""Call ``PATCH /domains/{domain_id}/config/{group}/{invalid}``."""
self.domain_config_api.create_config(self.domain['id'], self.config)
invalid_option = uuid.uuid4().hex
new_config = {'ldap': {invalid_option: uuid.uuid4().hex}}
# Trying to update an option that is neither whitelisted or sensitive
# should result in Forbidden.
self.patch(
'/domains/%(domain_id)s/config/ldap/%(invalid_option)s' % {
'domain_id': self.domain['id'],
'invalid_option': invalid_option},
body={'config': new_config},
expected_status=http_client.FORBIDDEN)
# Trying to update a valid option, but one that is not in the current
# config should result in NotFound
new_config = {'suffix': uuid.uuid4().hex}
self.patch(
'/domains/%(domain_id)s/config/ldap/suffix' % {
'domain_id': self.domain['id']},
body={'config': new_config},
expected_status=http_client.NOT_FOUND)
def test_update_config_invalid_option_invalid_domain(self):
"""Call ``PATCH /domains/{domain_id}/config/{group}/{invalid}``.
While updating Identity API-based domain config with an invalid option
and an invalid domain id provided, the request shall be rejected
with a response, 404 domain not found.
"""
self.domain_config_api.create_config(self.domain['id'], self.config)
invalid_option = uuid.uuid4().hex
new_config = {'ldap': {invalid_option: uuid.uuid4().hex}}
invalid_domain_id = uuid.uuid4().hex
self.patch(
'/domains/%(domain_id)s/config/ldap/%(invalid_option)s' % {
'domain_id': invalid_domain_id,
'invalid_option': invalid_option},
body={'config': new_config},
expected_status=exception.DomainNotFound.code)
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)
url = '/domains/config/default'
r = self.get(url)
default_config = r.result['config']
for group in default_config:
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_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)
url = '/domains/config/ldap/default'
r = self.get(url)
default_config = r.result['config']
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_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)
url = '/domains/config/ldap/url/default'
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_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/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_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)
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):
def setUp(self):
super(SecurityRequirementsTestCase, self).setUp()
# Create a user in the default domain
self.non_admin_user = unit.create_user(
self.identity_api,
CONF.identity.default_domain_id
)
# Create an admin in the default domain
self.admin_user = unit.create_user(
self.identity_api,
CONF.identity.default_domain_id
)
# Create a project in the default domain and a non-admin role
self.project = unit.new_project_ref(
domain_id=CONF.identity.default_domain_id
)
self.resource_api.create_project(self.project['id'], self.project)
self.non_admin_role = unit.new_role_ref(name='not_admin')
self.role_api.create_role(
self.non_admin_role['id'],
self.non_admin_role
)
# Give the non-admin user a role on the project
self.assignment_api.add_role_to_user_and_project(
self.non_admin_user['id'],
self.project['id'],
self.role['id']
)
# Give the user the admin role on the project, which is technically
# `self.role` because RestfulTestCase sets that up for us.
self.assignment_api.add_role_to_user_and_project(
self.admin_user['id'],
self.project['id'],
self.role_id
)
def _get_non_admin_token(self):
non_admin_auth_data = self.build_authentication_request(
user_id=self.non_admin_user['id'],
password=self.non_admin_user['password'],
project_id=self.project['id']
)
return self.get_requested_token(non_admin_auth_data)
def _get_admin_token(self):
non_admin_auth_data = self.build_authentication_request(
user_id=self.admin_user['id'],
password=self.admin_user['password'],
project_id=self.project['id']
)
return self.get_requested_token(non_admin_auth_data)
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
exist. Make sure when we ask for security compliance information, it's
only for the default domain and that it only returns whitelisted
options.
"""
password_regex = uuid.uuid4().hex
password_regex_description = uuid.uuid4().hex
self.config_fixture.config(
group='security_compliance',
password_regex=password_regex
)
self.config_fixture.config(
group='security_compliance',
password_regex_description=password_regex_description
)
expected_response = {
'security_compliance': {
'password_regex': password_regex,
'password_regex_description': password_regex_description
}
}
url = (
'/domains/%(domain_id)s/config/%(group)s' %
{
'domain_id': CONF.identity.default_domain_id,
'group': 'security_compliance',
}
)
# Make sure regular users and administrators can get security
# requirement information.
regular_response = self.get(url, token=self._get_non_admin_token())
self.assertEqual(regular_response.result['config'], expected_response)
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.
Support for enforcing security compliance rules per domain currently
does not exist, so exposing security compliance information for any
domain other than the default domain should not be allowed.
"""
# Create a new domain that is not the default domain
domain = unit.new_domain_ref()
self.resource_api.create_domain(domain['id'], domain)
# Set the security compliance configuration options
password_regex = uuid.uuid4().hex
password_regex_description = uuid.uuid4().hex
self.config_fixture.config(
group='security_compliance',
password_regex=password_regex
)
self.config_fixture.config(
group='security_compliance',
password_regex_description=password_regex_description
)
url = (
'/domains/%(domain_id)s/config/%(group)s' %
{
'domain_id': domain['id'],
'group': 'security_compliance',
}
)
# Make sure regular users and administrators are forbidden from doing
# this.
self.get(
url,
expected_status=http_client.FORBIDDEN,
token=self._get_non_admin_token()
)
self.get(
url,
expected_status=http_client.FORBIDDEN,
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.
Given that security compliance information is sensitive in nature, we
should make sure that only the options we want to expose are readable
via the API.
"""
# Set a security compliance configuration that isn't whitelisted
self.config_fixture.config(
group='security_compliance',
lockout_failure_attempts=1
)
url = (
'/domains/%(domain_id)s/config/%(group)s/%(option)s' %
{
'domain_id': CONF.identity.default_domain_id,
'group': 'security_compliance',
'option': 'lockout_failure_attempts'
}
)
# Make sure regular users and administrators are unable to ask for
# sensitive information.
self.get(
url,
expected_status=http_client.FORBIDDEN,
token=self._get_non_admin_token()
)
self.get(
url,
expected_status=http_client.FORBIDDEN,
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
self.config_fixture.config(
group='security_compliance',
password_regex=password_regex
)
group = 'security_compliance'
option = 'password_regex'
url = (
'/domains/%(domain_id)s/config/%(group)s/%(option)s' %
{
'domain_id': CONF.identity.default_domain_id,
'group': group,
'option': option
}
)
# Make sure regular users and administrators can ask for the
# password regular expression.
regular_response = self.get(url, token=self._get_non_admin_token())
self.assertEqual(
regular_response.result['config'][option],
password_regex
)
admin_response = self.get(url, token=self._get_admin_token())
self.assertEqual(
admin_response.result['config'][option],
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
self.config_fixture.config(
group='security_compliance',
password_regex_description=password_regex_description
)
group = 'security_compliance'
option = 'password_regex_description'
url = (
'/domains/%(domain_id)s/config/%(group)s/%(option)s' %
{
'domain_id': CONF.identity.default_domain_id,
'group': group,
'option': option
}
)
# Make sure regular users and administrators can ask for the
# password regular expression.
regular_response = self.get(url, token=self._get_non_admin_token())
self.assertEqual(
regular_response.result['config'][option],
password_regex_description
)
admin_response = self.get(url, token=self._get_admin_token())
self.assertEqual(
admin_response.result['config'][option],
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'
option = 'password_regex'
url = (
'/domains/%(domain_id)s/config/%(group)s/%(option)s' %
{
'domain_id': CONF.identity.default_domain_id,
'group': group,
'option': option
}
)
# Make sure regular users and administrators can ask for the password
# regular expression, but since it isn't set the returned value should
# be None.
regular_response = self.get(url, token=self._get_non_admin_token())
self.assertIsNone(regular_response.result['config'][option])
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'
option = 'password_regex_description'
url = (
'/domains/%(domain_id)s/config/%(group)s/%(option)s' %
{
'domain_id': CONF.identity.default_domain_id,
'group': group,
'option': option
}
)
# Make sure regular users and administrators can ask for the password
# regular expression description, but since it isn't set the returned
# value should be None.
regular_response = self.get(url, token=self._get_non_admin_token())
self.assertIsNone(regular_response.result['config'][option])
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.
Even though a user is in a separate domain, they should be able to see
the security requirements for the deployment. This is because security
compliance is not yet implemented on a per domain basis. Once that
happens, then this should no longer be possible since a user should
only care about the security compliance requirements for the domain
that they are in.
"""
# Make a new domain
domain = unit.new_domain_ref()
self.resource_api.create_domain(domain['id'], domain)
# Create a user in the new domain
user = unit.create_user(self.identity_api, domain['id'])
# Create a project in the new domain
project = unit.new_project_ref(domain_id=domain['id'])
self.resource_api.create_project(project['id'], project)
# Give the new user a non-admin role on the project
self.assignment_api.add_role_to_user_and_project(
user['id'],
project['id'],
self.non_admin_role['id']
)
# Set our security compliance config values, we do this after we've
# created our test user otherwise password validation will fail with a
# uuid type regex.
password_regex = uuid.uuid4().hex
password_regex_description = uuid.uuid4().hex
group = 'security_compliance'
self.config_fixture.config(
group=group,
password_regex=password_regex
)
self.config_fixture.config(
group=group,
password_regex_description=password_regex_description
)
# Get a token for the newly created user scoped to the project in the
# non-default domain and use it to get the password security
# requirements.
user_token = self.build_authentication_request(
user_id=user['id'],
password=user['password'],
project_id=project['id']
)
user_token = self.get_requested_token(user_token)
url = (
'/domains/%(domain_id)s/config/%(group)s' %
{
'domain_id': CONF.identity.default_domain_id,
'group': group,
}
)
response = self.get(url, token=user_token)
self.assertEqual(
response.result['config'][group]['password_regex'],
password_regex
)
self.assertEqual(
response.result['config'][group]['password_regex_description'],
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.
We should only allow the ability to modify a deployments security
compliance rules through configuration. Especially since it's only
enforced on the default domain.
"""
new_config = {
'security_compliance': {
'password_regex': uuid.uuid4().hex,
'password_regex_description': uuid.uuid4().hex
}
}
url = (
'/domains/%(domain_id)s/config/%(group)s' %
{
'domain_id': CONF.identity.default_domain_id,
'group': 'security_compliance',
}
)
# Make sure regular users and administrators aren't allowed to modify
# security compliance configuration through the API.
self.patch(
url,
body={'config': new_config},
expected_status=http_client.FORBIDDEN,
token=self._get_non_admin_token()
)
self.patch(
url,
body={'config': new_config},
expected_status=http_client.FORBIDDEN,
token=self._get_admin_token()
)
def test_update_security_compliance_password_regex_fails(self):
"""Make sure any updates to security compliance options fail."""
group = 'security_compliance'
option = 'password_regex'
url = (
'/domains/%(domain_id)s/config/%(group)s/%(option)s' %
{
'domain_id': CONF.identity.default_domain_id,
'group': group,
'option': option
}
)
new_config = {
group: {
option: uuid.uuid4().hex
}
}
# Make sure regular users and administrators aren't allowed to modify
# security compliance configuration through the API.
self.patch(
url,
body={'config': new_config},
expected_status=http_client.FORBIDDEN,
token=self._get_non_admin_token()
)
self.patch(
url,
body={'config': new_config},
expected_status=http_client.FORBIDDEN,
token=self._get_admin_token()
)
def test_update_security_compliance_password_regex_description_fails(self):
"""Make sure any updates to security compliance options fail."""
group = 'security_compliance'
option = 'password_regex_description'
url = (
'/domains/%(domain_id)s/config/%(group)s/%(option)s' %
{
'domain_id': CONF.identity.default_domain_id,
'group': group,
'option': option
}
)
new_config = {
group: {
option: uuid.uuid4().hex
}
}
# Make sure regular users and administrators aren't allowed to modify
# security compliance configuration through the API.
self.patch(
url,
body={'config': new_config},
expected_status=http_client.FORBIDDEN,
token=self._get_non_admin_token()
)
self.patch(
url,
body={'config': new_config},
expected_status=http_client.FORBIDDEN,
token=self._get_admin_token()
)
def test_update_non_whitelisted_security_compliance_option_fails(self):
"""Updating security compliance options through the API is not allowed.
Requests to update anything in the security compliance group through
the API should be Forbidden. This ensures that we are covering cases
where the option being updated isn't in the white list.
"""
group = 'security_compliance'
option = 'lockout_failure_attempts'
url = (
'/domains/%(domain_id)s/config/%(group)s/%(option)s' %
{
'domain_id': CONF.identity.default_domain_id,
'group': group,
'option': option
}
)
new_config = {
group: {
option: 1
}
}
# Make sure this behavior is not possible for regular users or
# administrators.
self.patch(
url,
body={'config': new_config},
expected_status=http_client.FORBIDDEN,
token=self._get_non_admin_token()
)
self.patch(
url,
body={'config': new_config},
expected_status=http_client.FORBIDDEN,
token=self._get_admin_token()
)
def test_delete_security_compliance_group_fails(self):
"""The security compliance group shouldn't be deleteable."""
url = (
'/domains/%(domain_id)s/config/%(group)s/' %
{
'domain_id': CONF.identity.default_domain_id,
'group': 'security_compliance',
}
)
# Make sure regular users and administrators can't delete the security
# compliance configuration group.
self.delete(
url,
expected_status=http_client.FORBIDDEN,
token=self._get_non_admin_token()
)
self.delete(
url,
expected_status=http_client.FORBIDDEN,
token=self._get_admin_token()
)
def test_delete_security_compliance_password_regex_fails(self):
"""The security compliance options shouldn't be deleteable."""
url = (
'/domains/%(domain_id)s/config/%(group)s/%(option)s' %
{
'domain_id': CONF.identity.default_domain_id,
'group': 'security_compliance',
'option': 'password_regex'
}
)
# Make sure regular users and administrators can't delete the security
# compliance configuration group.
self.delete(
url,
expected_status=http_client.FORBIDDEN,
token=self._get_non_admin_token()
)
self.delete(
url,
expected_status=http_client.FORBIDDEN,
token=self._get_admin_token()
)
def test_delete_security_compliance_password_regex_description_fails(self):
"""The security compliance options shouldn't be deleteable."""
url = (
'/domains/%(domain_id)s/config/%(group)s/%(option)s' %
{
'domain_id': CONF.identity.default_domain_id,
'group': 'security_compliance',
'option': 'password_regex_description'
}
)
# Make sure regular users and administrators can't delete the security
# compliance configuration group.
self.delete(
url,
expected_status=http_client.FORBIDDEN,
token=self._get_non_admin_token()
)
self.delete(
url,
expected_status=http_client.FORBIDDEN,
token=self._get_admin_token()
)
def test_delete_non_whitelisted_security_compliance_options_fails(self):
"""The security compliance options shouldn't be deleteable."""
url = (
'/domains/%(domain_id)s/config/%(group)s/%(option)s' %
{
'domain_id': CONF.identity.default_domain_id,
'group': 'security_compliance',
'option': 'lockout_failure_attempts'
}
)
# Make sure regular users and administrators can't delete the security
# compliance configuration group.
self.delete(
url,
expected_status=http_client.FORBIDDEN,
token=self._get_non_admin_token()
)
self.delete(
url,
expected_status=http_client.FORBIDDEN,
token=self._get_admin_token()
)