"Fossies" - the Fresh Open Source Software Archive

Member "keystone-17.0.0/keystone/tests/unit/test_v3_domain_config.py" (13 May 2020, 48095 Bytes) of package /linux/misc/openstack/keystone-17.0.0.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. See also the latest Fossies "Diffs" side-by-side code changes report for "test_v3_domain_config.py": 16.0.1_vs_17.0.0.

    1 # Licensed under the Apache License, Version 2.0 (the "License"); you may
    2 # not use this file except in compliance with the License. You may obtain
    3 # a copy of the License at
    4 #
    5 #      http://www.apache.org/licenses/LICENSE-2.0
    6 #
    7 # Unless required by applicable law or agreed to in writing, software
    8 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
    9 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
   10 # License for the specific language governing permissions and limitations
   11 # under the License.
   12 
   13 import copy
   14 import uuid
   15 
   16 import http.client
   17 
   18 from keystone.common import provider_api
   19 import keystone.conf
   20 from keystone import exception
   21 from keystone.tests import unit
   22 from keystone.tests.unit import test_v3
   23 
   24 
   25 CONF = keystone.conf.CONF
   26 PROVIDERS = provider_api.ProviderAPIs
   27 
   28 
   29 class DomainConfigTestCase(test_v3.RestfulTestCase):
   30     """Test domain config support."""
   31 
   32     def setUp(self):
   33         super(DomainConfigTestCase, self).setUp()
   34 
   35         self.domain = unit.new_domain_ref()
   36         PROVIDERS.resource_api.create_domain(self.domain['id'], self.domain)
   37         self.config = {'ldap': {'url': uuid.uuid4().hex,
   38                                 'user_tree_dn': uuid.uuid4().hex},
   39                        'identity': {'driver': uuid.uuid4().hex}}
   40 
   41     def test_create_config(self):
   42         """Call ``PUT /domains/{domain_id}/config``."""
   43         url = '/domains/%(domain_id)s/config' % {
   44             'domain_id': self.domain['id']}
   45         r = self.put(url, body={'config': self.config},
   46                      expected_status=http.client.CREATED)
   47         res = PROVIDERS.domain_config_api.get_config(self.domain['id'])
   48         self.assertEqual(self.config, r.result['config'])
   49         self.assertEqual(self.config, res)
   50 
   51     def test_create_config_invalid_domain(self):
   52         """Call ``PUT /domains/{domain_id}/config``.
   53 
   54         While creating Identity API-based domain config with an invalid domain
   55         id provided, the request shall be rejected with a response, 404 domain
   56         not found.
   57         """
   58         invalid_domain_id = uuid.uuid4().hex
   59         url = '/domains/%(domain_id)s/config' % {
   60             'domain_id': invalid_domain_id}
   61         self.put(url, body={'config': self.config},
   62                  expected_status=exception.DomainNotFound.code)
   63 
   64     def test_create_config_twice(self):
   65         """Check multiple creates don't throw error."""
   66         self.put('/domains/%(domain_id)s/config' % {
   67             'domain_id': self.domain['id']},
   68             body={'config': self.config},
   69             expected_status=http.client.CREATED)
   70         self.put('/domains/%(domain_id)s/config' % {
   71             'domain_id': self.domain['id']},
   72             body={'config': self.config},
   73             expected_status=http.client.OK)
   74 
   75     def test_delete_config(self):
   76         """Call ``DELETE /domains{domain_id}/config``."""
   77         PROVIDERS.domain_config_api.create_config(
   78             self.domain['id'], self.config
   79         )
   80         self.delete('/domains/%(domain_id)s/config' % {
   81             'domain_id': self.domain['id']})
   82         self.get('/domains/%(domain_id)s/config' % {
   83             'domain_id': self.domain['id']},
   84             expected_status=exception.DomainConfigNotFound.code)
   85 
   86     def test_delete_config_invalid_domain(self):
   87         """Call ``DELETE /domains{domain_id}/config``.
   88 
   89         While deleting Identity API-based domain config with an invalid domain
   90         id provided, the request shall be rejected with a response, 404 domain
   91         not found.
   92         """
   93         PROVIDERS.domain_config_api.create_config(
   94             self.domain['id'], self.config
   95         )
   96         invalid_domain_id = uuid.uuid4().hex
   97         self.delete('/domains/%(domain_id)s/config' % {
   98             'domain_id': invalid_domain_id},
   99             expected_status=exception.DomainNotFound.code)
  100 
  101     def test_delete_config_by_group(self):
  102         """Call ``DELETE /domains{domain_id}/config/{group}``."""
  103         PROVIDERS.domain_config_api.create_config(
  104             self.domain['id'], self.config
  105         )
  106         self.delete('/domains/%(domain_id)s/config/ldap' % {
  107             'domain_id': self.domain['id']})
  108         res = PROVIDERS.domain_config_api.get_config(self.domain['id'])
  109         self.assertNotIn('ldap', res)
  110 
  111     def test_delete_config_by_group_invalid_domain(self):
  112         """Call ``DELETE /domains{domain_id}/config/{group}``.
  113 
  114         While deleting Identity API-based domain config by group with an
  115         invalid domain id provided, the request shall be rejected with a
  116         response 404 domain not found.
  117         """
  118         PROVIDERS.domain_config_api.create_config(
  119             self.domain['id'], self.config
  120         )
  121         invalid_domain_id = uuid.uuid4().hex
  122         self.delete('/domains/%(domain_id)s/config/ldap' % {
  123             'domain_id': invalid_domain_id},
  124             expected_status=exception.DomainNotFound.code)
  125 
  126     def test_get_head_config(self):
  127         """Call ``GET & HEAD for /domains{domain_id}/config``."""
  128         PROVIDERS.domain_config_api.create_config(
  129             self.domain['id'], self.config
  130         )
  131         url = '/domains/%(domain_id)s/config' % {
  132             'domain_id': self.domain['id']}
  133         r = self.get(url)
  134         self.assertEqual(self.config, r.result['config'])
  135         self.head(url, expected_status=http.client.OK)
  136 
  137     def test_get_head_config_by_group(self):
  138         """Call ``GET & HEAD /domains{domain_id}/config/{group}``."""
  139         PROVIDERS.domain_config_api.create_config(
  140             self.domain['id'], self.config
  141         )
  142         url = '/domains/%(domain_id)s/config/ldap' % {
  143             'domain_id': self.domain['id']}
  144         r = self.get(url)
  145         self.assertEqual({'ldap': self.config['ldap']}, r.result['config'])
  146         self.head(url, expected_status=http.client.OK)
  147 
  148     def test_get_head_config_by_group_invalid_domain(self):
  149         """Call ``GET & HEAD /domains{domain_id}/config/{group}``.
  150 
  151         While retrieving Identity API-based domain config by group with an
  152         invalid domain id provided, the request shall be rejected with a
  153         response 404 domain not found.
  154         """
  155         PROVIDERS.domain_config_api.create_config(
  156             self.domain['id'], self.config
  157         )
  158         invalid_domain_id = uuid.uuid4().hex
  159         url = ('/domains/%(domain_id)s/config/ldap' % {
  160             'domain_id': invalid_domain_id}
  161         )
  162         self.get(url, expected_status=exception.DomainNotFound.code)
  163         self.head(url, expected_status=exception.DomainNotFound.code)
  164 
  165     def test_get_head_config_by_option(self):
  166         """Call ``GET & HEAD /domains{domain_id}/config/{group}/{option}``."""
  167         PROVIDERS.domain_config_api.create_config(
  168             self.domain['id'], self.config
  169         )
  170         url = '/domains/%(domain_id)s/config/ldap/url' % {
  171             'domain_id': self.domain['id']}
  172         r = self.get(url)
  173         self.assertEqual({'url': self.config['ldap']['url']},
  174                          r.result['config'])
  175         self.head(url, expected_status=http.client.OK)
  176 
  177     def test_get_head_config_by_option_invalid_domain(self):
  178         """Call ``GET & HEAD /domains{domain_id}/config/{group}/{option}``.
  179 
  180         While retrieving Identity API-based domain config by option with an
  181         invalid domain id provided, the request shall be rejected with a
  182         response 404 domain not found.
  183         """
  184         PROVIDERS.domain_config_api.create_config(
  185             self.domain['id'], self.config
  186         )
  187         invalid_domain_id = uuid.uuid4().hex
  188         url = ('/domains/%(domain_id)s/config/ldap/url' % {
  189             'domain_id': invalid_domain_id}
  190         )
  191         self.get(url, expected_status=exception.DomainNotFound.code)
  192         self.head(url, expected_status=exception.DomainNotFound.code)
  193 
  194     def test_get_head_non_existant_config(self):
  195         """Call ``GET /domains{domain_id}/config when no config defined``."""
  196         url = ('/domains/%(domain_id)s/config' % {
  197             'domain_id': self.domain['id']}
  198         )
  199         self.get(url, expected_status=http.client.NOT_FOUND)
  200         self.head(url, expected_status=http.client.NOT_FOUND)
  201 
  202     def test_get_head_non_existant_config_invalid_domain(self):
  203         """Call ``GET & HEAD /domains/{domain_id}/config with invalid domain``.
  204 
  205         While retrieving non-existent Identity API-based domain config with an
  206         invalid domain id provided, the request shall be rejected with a
  207         response 404 domain not found.
  208         """
  209         invalid_domain_id = uuid.uuid4().hex
  210         url = ('/domains/%(domain_id)s/config' % {
  211             'domain_id': invalid_domain_id}
  212         )
  213         self.get(url, expected_status=exception.DomainNotFound.code)
  214         self.head(url, expected_status=exception.DomainNotFound.code)
  215 
  216     def test_get_head_non_existant_config_group(self):
  217         """Call ``GET /domains/{domain_id}/config/{group_not_exist}``."""
  218         config = {'ldap': {'url': uuid.uuid4().hex}}
  219         PROVIDERS.domain_config_api.create_config(self.domain['id'], config)
  220         url = ('/domains/%(domain_id)s/config/identity' % {
  221             'domain_id': self.domain['id']}
  222         )
  223         self.get(url, expected_status=http.client.NOT_FOUND)
  224         self.head(url, expected_status=http.client.NOT_FOUND)
  225 
  226     def test_get_head_non_existant_config_group_invalid_domain(self):
  227         """Call ``GET & HEAD /domains/{domain_id}/config/{group}``.
  228 
  229         While retrieving non-existent Identity API-based domain config group
  230         with an invalid domain id provided, the request shall be rejected with
  231         a response, 404 domain not found.
  232         """
  233         config = {'ldap': {'url': uuid.uuid4().hex}}
  234         PROVIDERS.domain_config_api.create_config(self.domain['id'], config)
  235         invalid_domain_id = uuid.uuid4().hex
  236         url = ('/domains/%(domain_id)s/config/identity' % {
  237             'domain_id': invalid_domain_id}
  238         )
  239         self.get(url, expected_status=exception.DomainNotFound.code)
  240         self.head(url, expected_status=exception.DomainNotFound.code)
  241 
  242     def test_get_head_non_existant_config_option(self):
  243         """Test that Not Found is returned when option doesn't exist.
  244 
  245         Call ``GET & HEAD /domains/{domain_id}/config/{group}/{opt_not_exist}``
  246         and ensure a Not Found is returned because the option isn't defined
  247         within the group.
  248         """
  249         config = {'ldap': {'url': uuid.uuid4().hex}}
  250         PROVIDERS.domain_config_api.create_config(self.domain['id'], config)
  251         url = ('/domains/%(domain_id)s/config/ldap/user_tree_dn' % {
  252             'domain_id': self.domain['id']}
  253         )
  254         self.get(url, expected_status=http.client.NOT_FOUND)
  255         self.head(url, expected_status=http.client.NOT_FOUND)
  256 
  257     def test_get_head_non_existant_config_option_with_invalid_domain(self):
  258         """Test that Domain Not Found is returned with invalid domain.
  259 
  260         Call ``GET & HEAD /domains/{domain_id}/config/{group}/{opt_not_exist}``
  261 
  262         While retrieving non-existent Identity API-based domain config option
  263         with an invalid domain id provided, the request shall be rejected with
  264         a response, 404 domain not found.
  265         """
  266         config = {'ldap': {'url': uuid.uuid4().hex}}
  267         PROVIDERS.domain_config_api.create_config(self.domain['id'], config)
  268         invalid_domain_id = uuid.uuid4().hex
  269         url = ('/domains/%(domain_id)s/config/ldap/user_tree_dn' % {
  270             'domain_id': invalid_domain_id}
  271         )
  272         self.get(url, expected_status=exception.DomainNotFound.code)
  273         self.head(url, expected_status=exception.DomainNotFound.code)
  274 
  275     def test_update_config(self):
  276         """Call ``PATCH /domains/{domain_id}/config``."""
  277         PROVIDERS.domain_config_api.create_config(
  278             self.domain['id'], self.config
  279         )
  280         new_config = {'ldap': {'url': uuid.uuid4().hex},
  281                       'identity': {'driver': uuid.uuid4().hex}}
  282         r = self.patch('/domains/%(domain_id)s/config' % {
  283             'domain_id': self.domain['id']},
  284             body={'config': new_config})
  285         res = PROVIDERS.domain_config_api.get_config(self.domain['id'])
  286         expected_config = copy.deepcopy(self.config)
  287         expected_config['ldap']['url'] = new_config['ldap']['url']
  288         expected_config['identity']['driver'] = (
  289             new_config['identity']['driver'])
  290         self.assertEqual(expected_config, r.result['config'])
  291         self.assertEqual(expected_config, res)
  292 
  293     def test_update_config_invalid_domain(self):
  294         """Call ``PATCH /domains/{domain_id}/config``.
  295 
  296         While updating Identity API-based domain config with an invalid domain
  297         id provided, the request shall be rejected with a response, 404 domain
  298         not found.
  299         """
  300         PROVIDERS.domain_config_api.create_config(
  301             self.domain['id'], self.config
  302         )
  303         new_config = {'ldap': {'url': uuid.uuid4().hex},
  304                       'identity': {'driver': uuid.uuid4().hex}}
  305         invalid_domain_id = uuid.uuid4().hex
  306         self.patch('/domains/%(domain_id)s/config' % {
  307             'domain_id': invalid_domain_id},
  308             body={'config': new_config},
  309             expected_status=exception.DomainNotFound.code)
  310 
  311     def test_update_config_group(self):
  312         """Call ``PATCH /domains/{domain_id}/config/{group}``."""
  313         PROVIDERS.domain_config_api.create_config(
  314             self.domain['id'], self.config
  315         )
  316         new_config = {'ldap': {'url': uuid.uuid4().hex,
  317                                'user_filter': uuid.uuid4().hex}}
  318         r = self.patch('/domains/%(domain_id)s/config/ldap' % {
  319             'domain_id': self.domain['id']},
  320             body={'config': new_config})
  321         res = PROVIDERS.domain_config_api.get_config(self.domain['id'])
  322         expected_config = copy.deepcopy(self.config)
  323         expected_config['ldap']['url'] = new_config['ldap']['url']
  324         expected_config['ldap']['user_filter'] = (
  325             new_config['ldap']['user_filter'])
  326         self.assertEqual(expected_config, r.result['config'])
  327         self.assertEqual(expected_config, res)
  328 
  329     def test_update_config_group_invalid_domain(self):
  330         """Call ``PATCH /domains/{domain_id}/config/{group}``.
  331 
  332         While updating Identity API-based domain config group with an invalid
  333         domain id provided, the request shall be rejected with a response,
  334         404 domain not found.
  335         """
  336         PROVIDERS.domain_config_api.create_config(
  337             self.domain['id'], self.config
  338         )
  339         new_config = {'ldap': {'url': uuid.uuid4().hex,
  340                                'user_filter': uuid.uuid4().hex}}
  341         invalid_domain_id = uuid.uuid4().hex
  342         self.patch('/domains/%(domain_id)s/config/ldap' % {
  343             'domain_id': invalid_domain_id},
  344             body={'config': new_config},
  345             expected_status=exception.DomainNotFound.code)
  346 
  347     def test_update_config_invalid_group(self):
  348         """Call ``PATCH /domains/{domain_id}/config/{invalid_group}``."""
  349         PROVIDERS.domain_config_api.create_config(
  350             self.domain['id'], self.config
  351         )
  352 
  353         # Trying to update a group that is neither whitelisted or sensitive
  354         # should result in Forbidden.
  355         invalid_group = uuid.uuid4().hex
  356         new_config = {invalid_group: {'url': uuid.uuid4().hex,
  357                                       'user_filter': uuid.uuid4().hex}}
  358         self.patch('/domains/%(domain_id)s/config/%(invalid_group)s' % {
  359             'domain_id': self.domain['id'], 'invalid_group': invalid_group},
  360             body={'config': new_config},
  361             expected_status=http.client.FORBIDDEN)
  362         # Trying to update a valid group, but one that is not in the current
  363         # config should result in NotFound
  364         config = {'ldap': {'suffix': uuid.uuid4().hex}}
  365         PROVIDERS.domain_config_api.create_config(self.domain['id'], config)
  366         new_config = {'identity': {'driver': uuid.uuid4().hex}}
  367         self.patch('/domains/%(domain_id)s/config/identity' % {
  368             'domain_id': self.domain['id']},
  369             body={'config': new_config},
  370             expected_status=http.client.NOT_FOUND)
  371 
  372     def test_update_config_invalid_group_invalid_domain(self):
  373         """Call ``PATCH /domains/{domain_id}/config/{invalid_group}``.
  374 
  375         While updating Identity API-based domain config with an invalid group
  376         and an invalid domain id provided, the request shall be rejected
  377         with a response, 404 domain not found.
  378         """
  379         PROVIDERS.domain_config_api.create_config(
  380             self.domain['id'], self.config
  381         )
  382         invalid_group = uuid.uuid4().hex
  383         new_config = {invalid_group: {'url': uuid.uuid4().hex,
  384                                       'user_filter': uuid.uuid4().hex}}
  385         invalid_domain_id = uuid.uuid4().hex
  386         self.patch('/domains/%(domain_id)s/config/%(invalid_group)s' % {
  387             'domain_id': invalid_domain_id,
  388             'invalid_group': invalid_group},
  389             body={'config': new_config},
  390             expected_status=exception.DomainNotFound.code)
  391 
  392     def test_update_config_option(self):
  393         """Call ``PATCH /domains/{domain_id}/config/{group}/{option}``."""
  394         PROVIDERS.domain_config_api.create_config(
  395             self.domain['id'], self.config
  396         )
  397         new_config = {'url': uuid.uuid4().hex}
  398         r = self.patch('/domains/%(domain_id)s/config/ldap/url' % {
  399             'domain_id': self.domain['id']},
  400             body={'config': new_config})
  401         res = PROVIDERS.domain_config_api.get_config(self.domain['id'])
  402         expected_config = copy.deepcopy(self.config)
  403         expected_config['ldap']['url'] = new_config['url']
  404         self.assertEqual(expected_config, r.result['config'])
  405         self.assertEqual(expected_config, res)
  406 
  407     def test_update_config_option_invalid_domain(self):
  408         """Call ``PATCH /domains/{domain_id}/config/{group}/{option}``.
  409 
  410         While updating Identity API-based domain config option with an invalid
  411         domain id provided, the request shall be rejected with a response, 404
  412         domain not found.
  413         """
  414         PROVIDERS.domain_config_api.create_config(
  415             self.domain['id'], self.config
  416         )
  417         new_config = {'url': uuid.uuid4().hex}
  418         invalid_domain_id = uuid.uuid4().hex
  419         self.patch('/domains/%(domain_id)s/config/ldap/url' % {
  420             'domain_id': invalid_domain_id},
  421             body={'config': new_config},
  422             expected_status=exception.DomainNotFound.code)
  423 
  424     def test_update_config_invalid_option(self):
  425         """Call ``PATCH /domains/{domain_id}/config/{group}/{invalid}``."""
  426         PROVIDERS.domain_config_api.create_config(
  427             self.domain['id'], self.config
  428         )
  429         invalid_option = uuid.uuid4().hex
  430         new_config = {'ldap': {invalid_option: uuid.uuid4().hex}}
  431         # Trying to update an option that is neither whitelisted or sensitive
  432         # should result in Forbidden.
  433         self.patch(
  434             '/domains/%(domain_id)s/config/ldap/%(invalid_option)s' % {
  435                 'domain_id': self.domain['id'],
  436                 'invalid_option': invalid_option},
  437             body={'config': new_config},
  438             expected_status=http.client.FORBIDDEN)
  439         # Trying to update a valid option, but one that is not in the current
  440         # config should result in NotFound
  441         new_config = {'suffix': uuid.uuid4().hex}
  442         self.patch(
  443             '/domains/%(domain_id)s/config/ldap/suffix' % {
  444                 'domain_id': self.domain['id']},
  445             body={'config': new_config},
  446             expected_status=http.client.NOT_FOUND)
  447 
  448     def test_update_config_invalid_option_invalid_domain(self):
  449         """Call ``PATCH /domains/{domain_id}/config/{group}/{invalid}``.
  450 
  451         While updating Identity API-based domain config with an invalid option
  452         and an invalid domain id provided, the request shall be rejected
  453         with a response, 404 domain not found.
  454         """
  455         PROVIDERS.domain_config_api.create_config(
  456             self.domain['id'], self.config
  457         )
  458         invalid_option = uuid.uuid4().hex
  459         new_config = {'ldap': {invalid_option: uuid.uuid4().hex}}
  460         invalid_domain_id = uuid.uuid4().hex
  461         self.patch(
  462             '/domains/%(domain_id)s/config/ldap/%(invalid_option)s' % {
  463                 'domain_id': invalid_domain_id,
  464                 'invalid_option': invalid_option},
  465             body={'config': new_config},
  466             expected_status=exception.DomainNotFound.code)
  467 
  468     def test_get_head_config_default(self):
  469         """Call ``GET & HEAD /domains/config/default``."""
  470         # Create a config that overrides a few of the options so that we can
  471         # check that only the defaults are returned.
  472         PROVIDERS.domain_config_api.create_config(
  473             self.domain['id'], self.config
  474         )
  475         url = '/domains/config/default'
  476         r = self.get(url)
  477         default_config = r.result['config']
  478         for group in default_config:
  479             for option in default_config[group]:
  480                 self.assertEqual(getattr(getattr(CONF, group), option),
  481                                  default_config[group][option])
  482         self.head(url, expected_status=http.client.OK)
  483 
  484     def test_get_head_config_default_by_group(self):
  485         """Call ``GET & HEAD /domains/config/{group}/default``."""
  486         # Create a config that overrides a few of the options so that we can
  487         # check that only the defaults are returned.
  488         PROVIDERS.domain_config_api.create_config(
  489             self.domain['id'], self.config
  490         )
  491         url = '/domains/config/ldap/default'
  492         r = self.get(url)
  493         default_config = r.result['config']
  494         for option in default_config['ldap']:
  495             self.assertEqual(getattr(CONF.ldap, option),
  496                              default_config['ldap'][option])
  497         self.head(url, expected_status=http.client.OK)
  498 
  499     def test_get_head_config_default_by_option(self):
  500         """Call ``GET & HEAD /domains/config/{group}/{option}/default``."""
  501         # Create a config that overrides a few of the options so that we can
  502         # check that only the defaults are returned.
  503         PROVIDERS.domain_config_api.create_config(
  504             self.domain['id'], self.config
  505         )
  506         url = '/domains/config/ldap/url/default'
  507         r = self.get(url)
  508         default_config = r.result['config']
  509         self.assertEqual(CONF.ldap.url, default_config['url'])
  510         self.head(url, expected_status=http.client.OK)
  511 
  512     def test_get_head_config_default_by_invalid_group(self):
  513         """Call ``GET & HEAD for /domains/config/{bad-group}/default``."""
  514         # First try a valid group, but one we don't support for domain config
  515         self.get('/domains/config/resource/default',
  516                  expected_status=http.client.FORBIDDEN)
  517         self.head('/domains/config/resource/default',
  518                   expected_status=http.client.FORBIDDEN)
  519 
  520         # Now try a totally invalid group
  521         url = '/domains/config/%s/default' % uuid.uuid4().hex
  522         self.get(url, expected_status=http.client.FORBIDDEN)
  523         self.head(url, expected_status=http.client.FORBIDDEN)
  524 
  525     def test_get_head_config_default_for_unsupported_group(self):
  526         # It should not be possible to expose configuration information for
  527         # groups that the domain configuration API backlists explicitly. Doing
  528         # so would be a security vulnerability because it would leak sensitive
  529         # information over the API.
  530         self.get('/domains/config/ldap/password/default',
  531                  expected_status=http.client.FORBIDDEN)
  532         self.head('/domains/config/ldap/password/default',
  533                   expected_status=http.client.FORBIDDEN)
  534 
  535     def test_get_head_config_default_for_invalid_option(self):
  536         """Returning invalid configuration options is invalid."""
  537         url = '/domains/config/ldap/%s/default' % uuid.uuid4().hex
  538         self.get(url, expected_status=http.client.FORBIDDEN)
  539         self.head(url, expected_status=http.client.FORBIDDEN)
  540 
  541 
  542 class SecurityRequirementsTestCase(test_v3.RestfulTestCase):
  543 
  544     def setUp(self):
  545         super(SecurityRequirementsTestCase, self).setUp()
  546 
  547         # Create a user in the default domain
  548         self.non_admin_user = unit.create_user(
  549             PROVIDERS.identity_api,
  550             CONF.identity.default_domain_id
  551         )
  552 
  553         # Create an admin in the default domain
  554         self.admin_user = unit.create_user(
  555             PROVIDERS.identity_api,
  556             CONF.identity.default_domain_id
  557         )
  558 
  559         # Create a project in the default domain and a non-admin role
  560         self.project = unit.new_project_ref(
  561             domain_id=CONF.identity.default_domain_id
  562         )
  563         PROVIDERS.resource_api.create_project(self.project['id'], self.project)
  564         self.non_admin_role = unit.new_role_ref(name='not_admin')
  565         PROVIDERS.role_api.create_role(
  566             self.non_admin_role['id'],
  567             self.non_admin_role
  568         )
  569 
  570         # Give the non-admin user a role on the project
  571         PROVIDERS.assignment_api.add_role_to_user_and_project(
  572             self.non_admin_user['id'],
  573             self.project['id'],
  574             self.role['id']
  575         )
  576 
  577         # Give the user the admin role on the project, which is technically
  578         # `self.role` because RestfulTestCase sets that up for us.
  579         PROVIDERS.assignment_api.add_role_to_user_and_project(
  580             self.admin_user['id'],
  581             self.project['id'],
  582             self.role_id
  583         )
  584 
  585     def _get_non_admin_token(self):
  586         non_admin_auth_data = self.build_authentication_request(
  587             user_id=self.non_admin_user['id'],
  588             password=self.non_admin_user['password'],
  589             project_id=self.project['id']
  590         )
  591         return self.get_requested_token(non_admin_auth_data)
  592 
  593     def _get_admin_token(self):
  594         non_admin_auth_data = self.build_authentication_request(
  595             user_id=self.admin_user['id'],
  596             password=self.admin_user['password'],
  597             project_id=self.project['id']
  598         )
  599         return self.get_requested_token(non_admin_auth_data)
  600 
  601     def test_get_head_security_compliance_config_for_default_domain(self):
  602         """Ask for all security compliance configuration options.
  603 
  604         Support for enforcing security compliance per domain currently doesn't
  605         exist. Make sure when we ask for security compliance information, it's
  606         only for the default domain and that it only returns whitelisted
  607         options.
  608         """
  609         password_regex = uuid.uuid4().hex
  610         password_regex_description = uuid.uuid4().hex
  611         self.config_fixture.config(
  612             group='security_compliance',
  613             password_regex=password_regex
  614         )
  615         self.config_fixture.config(
  616             group='security_compliance',
  617             password_regex_description=password_regex_description
  618         )
  619         expected_response = {
  620             'security_compliance': {
  621                 'password_regex': password_regex,
  622                 'password_regex_description': password_regex_description
  623             }
  624         }
  625         url = (
  626             '/domains/%(domain_id)s/config/%(group)s' %
  627             {
  628                 'domain_id': CONF.identity.default_domain_id,
  629                 'group': 'security_compliance',
  630             }
  631         )
  632 
  633         # Make sure regular users and administrators can get security
  634         # requirement information.
  635         regular_response = self.get(url, token=self._get_non_admin_token())
  636         self.assertEqual(regular_response.result['config'], expected_response)
  637         admin_response = self.get(url, token=self._get_admin_token())
  638         self.assertEqual(admin_response.result['config'], expected_response)
  639 
  640         # Ensure HEAD requests behave the same way
  641         self.head(
  642             url,
  643             token=self._get_non_admin_token(),
  644             expected_status=http.client.OK
  645         )
  646         self.head(
  647             url,
  648             token=self._get_admin_token(),
  649             expected_status=http.client.OK
  650         )
  651 
  652     def test_get_security_compliance_config_for_non_default_domain_fails(self):
  653         """Getting security compliance opts for other domains should fail.
  654 
  655         Support for enforcing security compliance rules per domain currently
  656         does not exist, so exposing security compliance information for any
  657         domain other than the default domain should not be allowed.
  658         """
  659         # Create a new domain that is not the default domain
  660         domain = unit.new_domain_ref()
  661         PROVIDERS.resource_api.create_domain(domain['id'], domain)
  662 
  663         # Set the security compliance configuration options
  664         password_regex = uuid.uuid4().hex
  665         password_regex_description = uuid.uuid4().hex
  666         self.config_fixture.config(
  667             group='security_compliance',
  668             password_regex=password_regex
  669         )
  670         self.config_fixture.config(
  671             group='security_compliance',
  672             password_regex_description=password_regex_description
  673         )
  674         url = (
  675             '/domains/%(domain_id)s/config/%(group)s' %
  676             {
  677                 'domain_id': domain['id'],
  678                 'group': 'security_compliance',
  679             }
  680         )
  681 
  682         # Make sure regular users and administrators are forbidden from doing
  683         # this.
  684         self.get(
  685             url,
  686             expected_status=http.client.FORBIDDEN,
  687             token=self._get_non_admin_token()
  688         )
  689         self.get(
  690             url,
  691             expected_status=http.client.FORBIDDEN,
  692             token=self._get_admin_token()
  693         )
  694 
  695         # Ensure HEAD requests behave the same way
  696         self.head(
  697             url,
  698             expected_status=http.client.FORBIDDEN,
  699             token=self._get_non_admin_token()
  700         )
  701         self.head(
  702             url,
  703             expected_status=http.client.FORBIDDEN,
  704             token=self._get_admin_token()
  705         )
  706 
  707     def test_get_non_whitelisted_security_compliance_opt_fails(self):
  708         """We only support exposing a subset of security compliance options.
  709 
  710         Given that security compliance information is sensitive in nature, we
  711         should make sure that only the options we want to expose are readable
  712         via the API.
  713         """
  714         # Set a security compliance configuration that isn't whitelisted
  715         self.config_fixture.config(
  716             group='security_compliance',
  717             lockout_failure_attempts=1
  718         )
  719         url = (
  720             '/domains/%(domain_id)s/config/%(group)s/%(option)s' %
  721             {
  722                 'domain_id': CONF.identity.default_domain_id,
  723                 'group': 'security_compliance',
  724                 'option': 'lockout_failure_attempts'
  725             }
  726         )
  727 
  728         # Make sure regular users and administrators are unable to ask for
  729         # sensitive information.
  730         self.get(
  731             url,
  732             expected_status=http.client.FORBIDDEN,
  733             token=self._get_non_admin_token()
  734         )
  735         self.get(
  736             url,
  737             expected_status=http.client.FORBIDDEN,
  738             token=self._get_admin_token()
  739         )
  740 
  741         # Ensure HEAD requests behave the same way
  742         self.head(
  743             url,
  744             expected_status=http.client.FORBIDDEN,
  745             token=self._get_non_admin_token()
  746         )
  747         self.head(
  748             url,
  749             expected_status=http.client.FORBIDDEN,
  750             token=self._get_admin_token()
  751         )
  752 
  753     def test_get_security_compliance_password_regex(self):
  754         """Ask for the security compliance password regular expression."""
  755         password_regex = uuid.uuid4().hex
  756         self.config_fixture.config(
  757             group='security_compliance',
  758             password_regex=password_regex
  759         )
  760         group = 'security_compliance'
  761         option = 'password_regex'
  762         url = (
  763             '/domains/%(domain_id)s/config/%(group)s/%(option)s' %
  764             {
  765                 'domain_id': CONF.identity.default_domain_id,
  766                 'group': group,
  767                 'option': option
  768             }
  769         )
  770 
  771         # Make sure regular users and administrators can ask for the
  772         # password regular expression.
  773         regular_response = self.get(url, token=self._get_non_admin_token())
  774         self.assertEqual(
  775             regular_response.result['config'][option],
  776             password_regex
  777         )
  778         admin_response = self.get(url, token=self._get_admin_token())
  779         self.assertEqual(
  780             admin_response.result['config'][option],
  781             password_regex
  782         )
  783 
  784         # Ensure HEAD requests behave the same way
  785         self.head(
  786             url,
  787             token=self._get_non_admin_token(),
  788             expected_status=http.client.OK
  789         )
  790         self.head(
  791             url,
  792             token=self._get_admin_token(),
  793             expected_status=http.client.OK
  794         )
  795 
  796     def test_get_security_compliance_password_regex_description(self):
  797         """Ask for the security compliance password regex description."""
  798         password_regex_description = uuid.uuid4().hex
  799         self.config_fixture.config(
  800             group='security_compliance',
  801             password_regex_description=password_regex_description
  802         )
  803         group = 'security_compliance'
  804         option = 'password_regex_description'
  805         url = (
  806             '/domains/%(domain_id)s/config/%(group)s/%(option)s' %
  807             {
  808                 'domain_id': CONF.identity.default_domain_id,
  809                 'group': group,
  810                 'option': option
  811             }
  812         )
  813 
  814         # Make sure regular users and administrators can ask for the
  815         # password regular expression.
  816         regular_response = self.get(url, token=self._get_non_admin_token())
  817         self.assertEqual(
  818             regular_response.result['config'][option],
  819             password_regex_description
  820         )
  821         admin_response = self.get(url, token=self._get_admin_token())
  822         self.assertEqual(
  823             admin_response.result['config'][option],
  824             password_regex_description
  825         )
  826 
  827         # Ensure HEAD requests behave the same way
  828         self.head(
  829             url,
  830             token=self._get_non_admin_token(),
  831             expected_status=http.client.OK
  832         )
  833         self.head(
  834             url,
  835             token=self._get_admin_token(),
  836             expected_status=http.client.OK
  837         )
  838 
  839     def test_get_security_compliance_password_regex_returns_none(self):
  840         """When an option isn't set, we should explicitly return None."""
  841         group = 'security_compliance'
  842         option = 'password_regex'
  843         url = (
  844             '/domains/%(domain_id)s/config/%(group)s/%(option)s' %
  845             {
  846                 'domain_id': CONF.identity.default_domain_id,
  847                 'group': group,
  848                 'option': option
  849             }
  850         )
  851 
  852         # Make sure regular users and administrators can ask for the password
  853         # regular expression, but since it isn't set the returned value should
  854         # be None.
  855         regular_response = self.get(url, token=self._get_non_admin_token())
  856         self.assertIsNone(regular_response.result['config'][option])
  857         admin_response = self.get(url, token=self._get_admin_token())
  858         self.assertIsNone(admin_response.result['config'][option])
  859 
  860         # Ensure HEAD requests behave the same way
  861         self.head(
  862             url,
  863             token=self._get_non_admin_token(),
  864             expected_status=http.client.OK
  865         )
  866         self.head(
  867             url,
  868             token=self._get_admin_token(),
  869             expected_status=http.client.OK
  870         )
  871 
  872     def test_get_security_compliance_password_regex_desc_returns_none(self):
  873         """When an option isn't set, we should explicitly return None."""
  874         group = 'security_compliance'
  875         option = 'password_regex_description'
  876         url = (
  877             '/domains/%(domain_id)s/config/%(group)s/%(option)s' %
  878             {
  879                 'domain_id': CONF.identity.default_domain_id,
  880                 'group': group,
  881                 'option': option
  882             }
  883         )
  884 
  885         # Make sure regular users and administrators can ask for the password
  886         # regular expression description, but since it isn't set the returned
  887         # value should be None.
  888         regular_response = self.get(url, token=self._get_non_admin_token())
  889         self.assertIsNone(regular_response.result['config'][option])
  890         admin_response = self.get(url, token=self._get_admin_token())
  891         self.assertIsNone(admin_response.result['config'][option])
  892 
  893         # Ensure HEAD requests behave the same way
  894         self.head(
  895             url,
  896             token=self._get_non_admin_token(),
  897             expected_status=http.client.OK
  898         )
  899         self.head(
  900             url,
  901             token=self._get_admin_token(),
  902             expected_status=http.client.OK
  903         )
  904 
  905     def test_get_security_compliance_config_with_user_from_other_domain(self):
  906         """Make sure users from other domains can access password requirements.
  907 
  908         Even though a user is in a separate domain, they should be able to see
  909         the security requirements for the deployment. This is because security
  910         compliance is not yet implemented on a per domain basis. Once that
  911         happens, then this should no longer be possible since a user should
  912         only care about the security compliance requirements for the domain
  913         that they are in.
  914         """
  915         # Make a new domain
  916         domain = unit.new_domain_ref()
  917         PROVIDERS.resource_api.create_domain(domain['id'], domain)
  918 
  919         # Create a user in the new domain
  920         user = unit.create_user(PROVIDERS.identity_api, domain['id'])
  921 
  922         # Create a project in the new domain
  923         project = unit.new_project_ref(domain_id=domain['id'])
  924         PROVIDERS.resource_api.create_project(project['id'], project)
  925 
  926         # Give the new user a non-admin role on the project
  927         PROVIDERS.assignment_api.add_role_to_user_and_project(
  928             user['id'],
  929             project['id'],
  930             self.non_admin_role['id']
  931         )
  932 
  933         # Set our security compliance config values, we do this after we've
  934         # created our test user otherwise password validation will fail with a
  935         # uuid type regex.
  936         password_regex = uuid.uuid4().hex
  937         password_regex_description = uuid.uuid4().hex
  938         group = 'security_compliance'
  939         self.config_fixture.config(
  940             group=group,
  941             password_regex=password_regex
  942         )
  943         self.config_fixture.config(
  944             group=group,
  945             password_regex_description=password_regex_description
  946         )
  947 
  948         # Get a token for the newly created user scoped to the project in the
  949         # non-default domain and use it to get the password security
  950         # requirements.
  951         user_token = self.build_authentication_request(
  952             user_id=user['id'],
  953             password=user['password'],
  954             project_id=project['id']
  955         )
  956         user_token = self.get_requested_token(user_token)
  957         url = (
  958             '/domains/%(domain_id)s/config/%(group)s' %
  959             {
  960                 'domain_id': CONF.identity.default_domain_id,
  961                 'group': group,
  962             }
  963         )
  964         response = self.get(url, token=user_token)
  965         self.assertEqual(
  966             response.result['config'][group]['password_regex'],
  967             password_regex
  968         )
  969         self.assertEqual(
  970             response.result['config'][group]['password_regex_description'],
  971             password_regex_description
  972         )
  973 
  974         # Ensure HEAD requests behave the same way
  975         self.head(
  976             url,
  977             token=user_token,
  978             expected_status=http.client.OK
  979         )
  980 
  981     def test_update_security_compliance_config_group_fails(self):
  982         """Make sure that updates to the entire security group section fail.
  983 
  984         We should only allow the ability to modify a deployments security
  985         compliance rules through configuration. Especially since it's only
  986         enforced on the default domain.
  987         """
  988         new_config = {
  989             'security_compliance': {
  990                 'password_regex': uuid.uuid4().hex,
  991                 'password_regex_description': uuid.uuid4().hex
  992             }
  993         }
  994         url = (
  995             '/domains/%(domain_id)s/config/%(group)s' %
  996             {
  997                 'domain_id': CONF.identity.default_domain_id,
  998                 'group': 'security_compliance',
  999             }
 1000         )
 1001 
 1002         # Make sure regular users and administrators aren't allowed to modify
 1003         # security compliance configuration through the API.
 1004         self.patch(
 1005             url,
 1006             body={'config': new_config},
 1007             expected_status=http.client.FORBIDDEN,
 1008             token=self._get_non_admin_token()
 1009         )
 1010         self.patch(
 1011             url,
 1012             body={'config': new_config},
 1013             expected_status=http.client.FORBIDDEN,
 1014             token=self._get_admin_token()
 1015         )
 1016 
 1017     def test_update_security_compliance_password_regex_fails(self):
 1018         """Make sure any updates to security compliance options fail."""
 1019         group = 'security_compliance'
 1020         option = 'password_regex'
 1021         url = (
 1022             '/domains/%(domain_id)s/config/%(group)s/%(option)s' %
 1023             {
 1024                 'domain_id': CONF.identity.default_domain_id,
 1025                 'group': group,
 1026                 'option': option
 1027             }
 1028         )
 1029         new_config = {
 1030             group: {
 1031                 option: uuid.uuid4().hex
 1032             }
 1033         }
 1034 
 1035         # Make sure regular users and administrators aren't allowed to modify
 1036         # security compliance configuration through the API.
 1037         self.patch(
 1038             url,
 1039             body={'config': new_config},
 1040             expected_status=http.client.FORBIDDEN,
 1041             token=self._get_non_admin_token()
 1042         )
 1043         self.patch(
 1044             url,
 1045             body={'config': new_config},
 1046             expected_status=http.client.FORBIDDEN,
 1047             token=self._get_admin_token()
 1048         )
 1049 
 1050     def test_update_security_compliance_password_regex_description_fails(self):
 1051         """Make sure any updates to security compliance options fail."""
 1052         group = 'security_compliance'
 1053         option = 'password_regex_description'
 1054         url = (
 1055             '/domains/%(domain_id)s/config/%(group)s/%(option)s' %
 1056             {
 1057                 'domain_id': CONF.identity.default_domain_id,
 1058                 'group': group,
 1059                 'option': option
 1060             }
 1061         )
 1062         new_config = {
 1063             group: {
 1064                 option: uuid.uuid4().hex
 1065             }
 1066         }
 1067 
 1068         # Make sure regular users and administrators aren't allowed to modify
 1069         # security compliance configuration through the API.
 1070         self.patch(
 1071             url,
 1072             body={'config': new_config},
 1073             expected_status=http.client.FORBIDDEN,
 1074             token=self._get_non_admin_token()
 1075         )
 1076         self.patch(
 1077             url,
 1078             body={'config': new_config},
 1079             expected_status=http.client.FORBIDDEN,
 1080             token=self._get_admin_token()
 1081         )
 1082 
 1083     def test_update_non_whitelisted_security_compliance_option_fails(self):
 1084         """Updating security compliance options through the API is not allowed.
 1085 
 1086         Requests to update anything in the security compliance group through
 1087         the API should be Forbidden. This ensures that we are covering cases
 1088         where the option being updated isn't in the white list.
 1089         """
 1090         group = 'security_compliance'
 1091         option = 'lockout_failure_attempts'
 1092         url = (
 1093             '/domains/%(domain_id)s/config/%(group)s/%(option)s' %
 1094             {
 1095                 'domain_id': CONF.identity.default_domain_id,
 1096                 'group': group,
 1097                 'option': option
 1098             }
 1099         )
 1100         new_config = {
 1101             group: {
 1102                 option: 1
 1103             }
 1104         }
 1105 
 1106         # Make sure this behavior is not possible for regular users or
 1107         # administrators.
 1108         self.patch(
 1109             url,
 1110             body={'config': new_config},
 1111             expected_status=http.client.FORBIDDEN,
 1112             token=self._get_non_admin_token()
 1113         )
 1114         self.patch(
 1115             url,
 1116             body={'config': new_config},
 1117             expected_status=http.client.FORBIDDEN,
 1118             token=self._get_admin_token()
 1119         )
 1120 
 1121     def test_delete_security_compliance_group_fails(self):
 1122         """The security compliance group shouldn't be deleteable."""
 1123         url = (
 1124             '/domains/%(domain_id)s/config/%(group)s/' %
 1125             {
 1126                 'domain_id': CONF.identity.default_domain_id,
 1127                 'group': 'security_compliance',
 1128             }
 1129         )
 1130 
 1131         # Make sure regular users and administrators can't delete the security
 1132         # compliance configuration group.
 1133         self.delete(
 1134             url,
 1135             expected_status=http.client.FORBIDDEN,
 1136             token=self._get_non_admin_token()
 1137         )
 1138         self.delete(
 1139             url,
 1140             expected_status=http.client.FORBIDDEN,
 1141             token=self._get_admin_token()
 1142         )
 1143 
 1144     def test_delete_security_compliance_password_regex_fails(self):
 1145         """The security compliance options shouldn't be deleteable."""
 1146         url = (
 1147             '/domains/%(domain_id)s/config/%(group)s/%(option)s' %
 1148             {
 1149                 'domain_id': CONF.identity.default_domain_id,
 1150                 'group': 'security_compliance',
 1151                 'option': 'password_regex'
 1152             }
 1153         )
 1154 
 1155         # Make sure regular users and administrators can't delete the security
 1156         # compliance configuration group.
 1157         self.delete(
 1158             url,
 1159             expected_status=http.client.FORBIDDEN,
 1160             token=self._get_non_admin_token()
 1161         )
 1162         self.delete(
 1163             url,
 1164             expected_status=http.client.FORBIDDEN,
 1165             token=self._get_admin_token()
 1166         )
 1167 
 1168     def test_delete_security_compliance_password_regex_description_fails(self):
 1169         """The security compliance options shouldn't be deleteable."""
 1170         url = (
 1171             '/domains/%(domain_id)s/config/%(group)s/%(option)s' %
 1172             {
 1173                 'domain_id': CONF.identity.default_domain_id,
 1174                 'group': 'security_compliance',
 1175                 'option': 'password_regex_description'
 1176             }
 1177         )
 1178 
 1179         # Make sure regular users and administrators can't delete the security
 1180         # compliance configuration group.
 1181         self.delete(
 1182             url,
 1183             expected_status=http.client.FORBIDDEN,
 1184             token=self._get_non_admin_token()
 1185         )
 1186         self.delete(
 1187             url,
 1188             expected_status=http.client.FORBIDDEN,
 1189             token=self._get_admin_token()
 1190         )
 1191 
 1192     def test_delete_non_whitelisted_security_compliance_options_fails(self):
 1193         """The security compliance options shouldn't be deleteable."""
 1194         url = (
 1195             '/domains/%(domain_id)s/config/%(group)s/%(option)s' %
 1196             {
 1197                 'domain_id': CONF.identity.default_domain_id,
 1198                 'group': 'security_compliance',
 1199                 'option': 'lockout_failure_attempts'
 1200             }
 1201         )
 1202 
 1203         # Make sure regular users and administrators can't delete the security
 1204         # compliance configuration group.
 1205         self.delete(
 1206             url,
 1207             expected_status=http.client.FORBIDDEN,
 1208             token=self._get_non_admin_token()
 1209         )
 1210         self.delete(
 1211             url,
 1212             expected_status=http.client.FORBIDDEN,
 1213             token=self._get_admin_token()
 1214         )