diff --git a/openstackclient/identity/v3/mapping.py b/openstackclient/identity/v3/mapping.py new file mode 100644 index 0000000000..ae5e03bd2d --- /dev/null +++ b/openstackclient/identity/v3/mapping.py @@ -0,0 +1,209 @@ +# Copyright 2014 CERN +# +# 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. +# + +"""Identity v3 federation mapping action implementations""" + +import json +import logging + +from cliff import command +from cliff import lister +from cliff import show +import six + +from openstackclient.common import exceptions +from openstackclient.common import utils + + +class _RulesReader(object): + """Helper class capable of reading rules from files""" + + def _read_rules(self, path): + """Read and parse rules from path + + Expect the file to contain a valid JSON structure. + + :param path: path to the file + :return: loaded and valid dictionary with rules + :raises exception.CommandError: In case the file cannot be + accessed or the content is not a valid JSON. + + Example of the content of the file: + [ + { + "local": [ + { + "group": { + "id": "85a868" + } + } + ], + "remote": [ + { + "type": "orgPersonType", + "any_one_of": [ + "Employee" + ] + }, + { + "type": "sn", + "any_one_of": [ + "Young" + ] + } + ] + } + ] + + """ + blob = utils.read_blob_file_contents(path) + try: + rules = json.loads(blob) + except ValueError as e: + raise exceptions.CommandError( + 'An error occurred when reading ' + 'rules from file %s: %s' % (path, e)) + else: + return rules + + +class CreateMapping(show.ShowOne, _RulesReader): + """Create new federation mapping""" + + log = logging.getLogger(__name__ + '.CreateMapping') + + def get_parser(self, prog_name): + parser = super(CreateMapping, self).get_parser(prog_name) + parser.add_argument( + 'mapping', + metavar='', + help='New mapping (must be unique)', + ) + parser.add_argument( + '--rules', + metavar='', required=True, + help='Filename with rules', + ) + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)' % parsed_args) + identity_client = self.app.client_manager.identity + + rules = self._read_rules(parsed_args.rules) + mapping = identity_client.federation.mappings.create( + mapping_id=parsed_args.mapping, + rules=rules) + + info = {} + info.update(mapping._info) + return zip(*sorted(six.iteritems(info))) + + +class DeleteMapping(command.Command): + """Delete federation mapping""" + + log = logging.getLogger(__name__ + '.DeleteMapping') + + def get_parser(self, prog_name): + parser = super(DeleteMapping, self).get_parser(prog_name) + parser.add_argument( + 'mapping', + metavar='', + help='Mapping to delete', + ) + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)' % parsed_args) + identity_client = self.app.client_manager.identity + + identity_client.federation.mappings.delete(parsed_args.mapping) + return + + +class ListMapping(lister.Lister): + """List federation mappings""" + log = logging.getLogger(__name__ + '.ListMapping') + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)' % parsed_args) + # NOTE(marek-denis): Since rules can be long and tedious I have decided + # to only list ids of the mappings. If somebody wants to check the + # rules, (s)he should show specific ones. + identity_client = self.app.client_manager.identity + data = identity_client.federation.mappings.list() + columns = ('ID',) + items = [utils.get_item_properties(s, columns) for s in data] + return (columns, items) + + +class SetMapping(show.ShowOne, _RulesReader): + """Update federation mapping""" + + log = logging.getLogger(__name__ + '.SetMapping') + + def get_parser(self, prog_name): + parser = super(SetMapping, self).get_parser(prog_name) + parser.add_argument( + 'mapping', + metavar='', + help='Mapping to update.', + ) + parser.add_argument( + '--rules', + metavar='', required=True, + help='Filename with rules', + ) + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)' % parsed_args) + identity_client = self.app.client_manager.identity + + rules = self._read_rules(parsed_args.rules) + + mapping = identity_client.federation.mappings.update( + mapping=parsed_args.mapping, + rules=rules) + + info = {} + info.update(mapping._info) + return zip(*sorted(six.iteritems(info))) + + +class ShowMapping(show.ShowOne): + """Show federation mapping details""" + + log = logging.getLogger(__name__ + '.ShowMapping') + + def get_parser(self, prog_name): + parser = super(ShowMapping, self).get_parser(prog_name) + parser.add_argument( + 'mapping', + metavar='', + help='Mapping to show', + ) + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)' % parsed_args) + identity_client = self.app.client_manager.identity + + mapping = identity_client.federation.mappings.get(parsed_args.mapping) + + info = {} + info.update(mapping._info) + return zip(*sorted(six.iteritems(info))) diff --git a/openstackclient/tests/identity/v3/fakes.py b/openstackclient/tests/identity/v3/fakes.py index e9cda9ffbc..a88a905e6f 100644 --- a/openstackclient/tests/identity/v3/fakes.py +++ b/openstackclient/tests/identity/v3/fakes.py @@ -38,6 +38,65 @@ GROUP = { 'name': group_name, } +mapping_id = 'test_mapping' +mapping_rules_file_path = '/tmp/path/to/file' +# Copied from +# (https://github.com/openstack/keystone/blob\ +# master/keystone/tests/mapping_fixtures.py +EMPLOYEE_GROUP_ID = "0cd5e9" +DEVELOPER_GROUP_ID = "xyz" +MAPPING_RULES = [ + { + "local": [ + { + "group": { + "id": EMPLOYEE_GROUP_ID + } + } + ], + "remote": [ + { + "type": "orgPersonType", + "not_any_of": [ + "Contractor", + "Guest" + ] + } + ] + } +] + +MAPPING_RULES_2 = [ + { + "local": [ + { + "group": { + "id": DEVELOPER_GROUP_ID + } + } + ], + "remote": [ + { + "type": "orgPersonType", + "any_one_of": [ + "Contractor" + ] + } + ] + } +] + + +MAPPING_RESPONSE = { + "id": mapping_id, + "rules": MAPPING_RULES +} + +MAPPING_RESPONSE_2 = { + "id": mapping_id, + "rules": MAPPING_RULES_2 +} + project_id = '8-9-64' project_name = 'beatles' project_description = 'Fab Four' @@ -224,6 +283,8 @@ class FakeFederationManager(object): def __init__(self, **kwargs): self.identity_providers = mock.Mock() self.identity_providers.resource_class = fakes.FakeResource(None, {}) + self.mappings = mock.Mock() + self.mappings.resource_class = fakes.FakeResource(None, {}) class FakeFederatedClient(FakeIdentityv3Client): diff --git a/openstackclient/tests/identity/v3/test_mappings.py b/openstackclient/tests/identity/v3/test_mappings.py new file mode 100644 index 0000000000..f5c318fefd --- /dev/null +++ b/openstackclient/tests/identity/v3/test_mappings.py @@ -0,0 +1,239 @@ +# Copyright 2014 CERN. +# +# 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 mock + +from openstackclient.common import exceptions +from openstackclient.identity.v3 import mapping +from openstackclient.tests import fakes +from openstackclient.tests.identity.v3 import fakes as identity_fakes + + +class TestMapping(identity_fakes.TestFederatedIdentity): + + def setUp(self): + super(TestMapping, self).setUp() + + federation_lib = self.app.client_manager.identity.federation + self.mapping_mock = federation_lib.mappings + self.mapping_mock.reset_mock() + + +class TestMappingCreate(TestMapping): + def setUp(self): + super(TestMappingCreate, self).setUp() + self.mapping_mock.create.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.MAPPING_RESPONSE), + loaded=True + ) + self.cmd = mapping.CreateMapping(self.app, None) + + def test_create_mapping(self): + arglist = [ + '--rules', identity_fakes.mapping_rules_file_path, + identity_fakes.mapping_id + ] + verifylist = [ + ('mapping', identity_fakes.mapping_id), + ('rules', identity_fakes.mapping_rules_file_path) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + mocker = mock.Mock() + mocker.return_value = identity_fakes.MAPPING_RULES + with mock.patch("openstackclient.identity.v3.mapping." + "CreateMapping._read_rules", mocker): + columns, data = self.cmd.take_action(parsed_args) + + self.mapping_mock.create.assert_called_with( + mapping_id=identity_fakes.mapping_id, + rules=identity_fakes.MAPPING_RULES) + + collist = ('id', 'rules') + self.assertEqual(columns, collist) + + datalist = (identity_fakes.mapping_id, + identity_fakes.MAPPING_RULES) + self.assertEqual(datalist, data) + + +class TestMappingDelete(TestMapping): + def setUp(self): + super(TestMappingDelete, self).setUp() + self.mapping_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.MAPPING_RESPONSE), + loaded=True) + + self.mapping_mock.delete.return_value = None + self.cmd = mapping.DeleteMapping(self.app, None) + + def test_delete_mapping(self): + arglist = [ + identity_fakes.mapping_id + ] + verifylist = [ + ('mapping', identity_fakes.mapping_id) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + self.mapping_mock.delete.assert_called_with( + identity_fakes.mapping_id) + + +class TestMappingList(TestMapping): + def setUp(self): + super(TestMappingList, self).setUp() + self.mapping_mock.get.return_value = fakes.FakeResource( + None, + {'id': identity_fakes.mapping_id}, + loaded=True) + # Pretend list command returns list of two mappings. + # NOTE(marek-denis): We are returning FakeResources with mapping id + # only as ShowMapping class is implemented in a way where rules will + # not be displayed, only mapping ids. + self.mapping_mock.list.return_value = [ + fakes.FakeResource( + None, + {'id': identity_fakes.mapping_id}, + loaded=True, + ), + fakes.FakeResource( + None, + {'id': 'extra_mapping'}, + loaded=True, + ), + ] + + # Get the command object to test + self.cmd = mapping.ListMapping(self.app, None) + + def test_mapping_list(self): + arglist = [] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.mapping_mock.list.assert_called_with() + + collist = ('ID',) + self.assertEqual(columns, collist) + + datalist = [(identity_fakes.mapping_id,), ('extra_mapping',)] + self.assertEqual(datalist, data) + + +class TestMappingShow(TestMapping): + def setUp(self): + super(TestMappingShow, self).setUp() + + self.mapping_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.MAPPING_RESPONSE), + loaded=True + ) + + self.cmd = mapping.ShowMapping(self.app, None) + + def test_mapping_show(self): + arglist = [ + identity_fakes.mapping_id + ] + verifylist = [ + ('mapping', identity_fakes.mapping_id) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.mapping_mock.get.assert_called_with( + identity_fakes.mapping_id) + + collist = ('id', 'rules') + self.assertEqual(columns, collist) + + datalist = (identity_fakes.mapping_id, + identity_fakes.MAPPING_RULES) + self.assertEqual(datalist, data) + + +class TestMappingSet(TestMapping): + + def setUp(self): + super(TestMappingSet, self).setUp() + + self.mapping_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.MAPPING_RESPONSE), + loaded=True + ) + + self.mapping_mock.update.return_value = fakes.FakeResource( + None, + identity_fakes.MAPPING_RESPONSE_2, + loaded=True + ) + + # Get the command object to test + self.cmd = mapping.SetMapping(self.app, None) + + def test_set_new_rules(self): + arglist = [ + '--rules', identity_fakes.mapping_rules_file_path, + identity_fakes.mapping_id + ] + verifylist = [ + ('mapping', identity_fakes.mapping_id), + ('rules', identity_fakes.mapping_rules_file_path) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + mocker = mock.Mock() + mocker.return_value = identity_fakes.MAPPING_RULES_2 + with mock.patch("openstackclient.identity.v3.mapping." + "SetMapping._read_rules", mocker): + columns, data = self.cmd.take_action(parsed_args) + self.mapping_mock.update.assert_called_with( + mapping=identity_fakes.mapping_id, + rules=identity_fakes.MAPPING_RULES_2) + + collist = ('id', 'rules') + self.assertEqual(columns, collist) + datalist = (identity_fakes.mapping_id, + identity_fakes.MAPPING_RULES_2) + self.assertEqual(datalist, data) + + def test_set_rules_wrong_file_path(self): + arglist = [ + '--rules', identity_fakes.mapping_rules_file_path, + identity_fakes.mapping_id + ] + verifylist = [ + ('mapping', identity_fakes.mapping_id), + ('rules', identity_fakes.mapping_rules_file_path) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) diff --git a/setup.cfg b/setup.cfg index 3178fe4467..d601fdfa22 100644 --- a/setup.cfg +++ b/setup.cfg @@ -212,6 +212,12 @@ openstack.identity.v3 = identity_provider_set = openstackclient.identity.v3.identity_provider:SetIdentityProvider identity_provider_show = openstackclient.identity.v3.identity_provider:ShowIdentityProvider + mapping_create = openstackclient.identity.v3.mapping:CreateMapping + mapping_delete = openstackclient.identity.v3.mapping:DeleteMapping + mapping_list = openstackclient.identity.v3.mapping:ListMapping + mapping_set = openstackclient.identity.v3.mapping:SetMapping + mapping_show = openstackclient.identity.v3.mapping:ShowMapping + policy_create = openstackclient.identity.v3.policy:CreatePolicy policy_delete = openstackclient.identity.v3.policy:DeletePolicy policy_list = openstackclient.identity.v3.policy:ListPolicy