diff options
| author | Colleen Murphy <colleen.murphy@suse.de> | 2019-08-21 17:38:29 -0700 |
|---|---|---|
| committer | Colleen Murphy <colleen.murphy@suse.com> | 2020-01-17 11:14:51 -0800 |
| commit | 70ab3f9dd56a638cdff516ca85baa5ebd64c888b (patch) | |
| tree | d8a92201238b7bcc749c80bb2d8a403f3d3b2d1b /openstackclient | |
| parent | db29e28b7c1a6ef737f0c4cd459906379f59b252 (diff) | |
| download | python-openstackclient-70ab3f9dd56a638cdff516ca85baa5ebd64c888b.tar.gz | |
Add support for app cred access rules
This commit introduces the --access-rules option for 'application
credential create' as well as new 'access rule' commands for listing,
showing, and deleting access rules.
bp whitelist-extension-for-app-creds
Change-Id: I04834b2874ec2a70da456a380b5bef03a392effa
Diffstat (limited to 'openstackclient')
5 files changed, 468 insertions, 9 deletions
diff --git a/openstackclient/identity/v3/access_rule.py b/openstackclient/identity/v3/access_rule.py new file mode 100644 index 00000000..d96b44da --- /dev/null +++ b/openstackclient/identity/v3/access_rule.py @@ -0,0 +1,118 @@ +# Copyright 2019 SUSE LLC +# +# 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 Access Rule action implementations""" + +import logging + +from osc_lib.command import command +from osc_lib import exceptions +from osc_lib import utils +import six + +from openstackclient.i18n import _ +from openstackclient.identity import common + + +LOG = logging.getLogger(__name__) + + +class DeleteAccessRule(command.Command): + _description = _("Delete access rule(s)") + + def get_parser(self, prog_name): + parser = super(DeleteAccessRule, self).get_parser(prog_name) + parser.add_argument( + 'access_rule', + metavar='<access-rule>', + nargs="+", + help=_('Application credentials(s) to delete (name or ID)'), + ) + return parser + + def take_action(self, parsed_args): + identity_client = self.app.client_manager.identity + + errors = 0 + for ac in parsed_args.access_rule: + try: + access_rule = utils.find_resource( + identity_client.access_rules, ac) + identity_client.access_rules.delete(access_rule.id) + except Exception as e: + errors += 1 + LOG.error(_("Failed to delete access rule with " + "ID '%(ac)s': %(e)s"), + {'ac': ac, 'e': e}) + + if errors > 0: + total = len(parsed_args.access_rule) + msg = (_("%(errors)s of %(total)s access rules failed " + "to delete.") % {'errors': errors, 'total': total}) + raise exceptions.CommandError(msg) + + +class ListAccessRule(command.Lister): + _description = _("List access rules") + + def get_parser(self, prog_name): + parser = super(ListAccessRule, self).get_parser(prog_name) + parser.add_argument( + '--user', + metavar='<user>', + help=_('User whose access rules to list (name or ID)'), + ) + common.add_user_domain_option_to_parser(parser) + return parser + + def take_action(self, parsed_args): + identity_client = self.app.client_manager.identity + if parsed_args.user: + user_id = common.find_user(identity_client, + parsed_args.user, + parsed_args.user_domain).id + else: + user_id = None + + columns = ('ID', 'Service', 'Method', 'Path') + data = identity_client.access_rules.list( + user=user_id) + return (columns, + (utils.get_item_properties( + s, columns, + formatters={}, + ) for s in data)) + + +class ShowAccessRule(command.ShowOne): + _description = _("Display access rule details") + + def get_parser(self, prog_name): + parser = super(ShowAccessRule, self).get_parser(prog_name) + parser.add_argument( + 'access_rule', + metavar='<access-rule>', + help=_('Application credential to display (name or ID)'), + ) + return parser + + def take_action(self, parsed_args): + identity_client = self.app.client_manager.identity + access_rule = utils.find_resource(identity_client.access_rules, + parsed_args.access_rule) + + access_rule._info.pop('links', None) + + return zip(*sorted(six.iteritems(access_rule._info))) diff --git a/openstackclient/identity/v3/application_credential.py b/openstackclient/identity/v3/application_credential.py index ea0b30cd..a2089856 100644 --- a/openstackclient/identity/v3/application_credential.py +++ b/openstackclient/identity/v3/application_credential.py @@ -16,6 +16,7 @@ """Identity v3 Application Credential action implementations""" import datetime +import json import logging from osc_lib.command import command @@ -79,6 +80,17 @@ class CreateApplicationCredential(command.ShowOne): ' other application credentials and trusts (this is the' ' default behavior)'), ) + parser.add_argument( + '--access-rules', + metavar='<access-rules>', + help=_('Either a string or file path containing a JSON-formatted ' + 'list of access rules, each containing a request method, ' + 'path, and service, for example ' + '\'[{"method": "GET", ' + '"path": "/v2.1/servers", ' + '"service": "compute"}]\''), + + ) return parser def take_action(self, parsed_args): @@ -105,6 +117,20 @@ class CreateApplicationCredential(command.ShowOne): else: unrestricted = parsed_args.unrestricted + if parsed_args.access_rules: + try: + access_rules = json.loads(parsed_args.access_rules) + except ValueError: + try: + with open(parsed_args.access_rules) as f: + access_rules = json.load(f) + except IOError: + raise exceptions.CommandError( + _("Access rules is not valid JSON string or file does" + " not exist.")) + else: + access_rules = None + app_cred_manager = identity_client.application_credentials application_credential = app_cred_manager.create( parsed_args.name, @@ -113,6 +139,7 @@ class CreateApplicationCredential(command.ShowOne): description=parsed_args.description, secret=parsed_args.secret, unrestricted=unrestricted, + access_rules=access_rules, ) application_credential._info.pop('links', None) diff --git a/openstackclient/tests/unit/identity/v3/fakes.py b/openstackclient/tests/unit/identity/v3/fakes.py index c394ab82..fc4a48e3 100644 --- a/openstackclient/tests/unit/identity/v3/fakes.py +++ b/openstackclient/tests/unit/identity/v3/fakes.py @@ -470,6 +470,14 @@ app_cred_description = 'app credential for testing' app_cred_expires = datetime.datetime(2022, 1, 1, 0, 0) app_cred_expires_str = app_cred_expires.strftime('%Y-%m-%dT%H:%M:%S%z') app_cred_secret = 'moresecuresecret' +app_cred_access_rules = ( + '[{"path": "/v2.1/servers", "method": "GET", "service": "compute"}]' +) +app_cred_access_rules_path = '/tmp/access_rules.json' +access_rule_id = 'access-rule-id' +access_rule_service = 'compute' +access_rule_path = '/v2.1/servers' +access_rule_method = 'GET' APP_CRED_BASIC = { 'id': app_cred_id, 'name': app_cred_name, @@ -478,7 +486,8 @@ APP_CRED_BASIC = { 'description': None, 'expires_at': None, 'unrestricted': False, - 'secret': app_cred_secret + 'secret': app_cred_secret, + 'access_rules': None } APP_CRED_OPTIONS = { 'id': app_cred_id, @@ -488,7 +497,25 @@ APP_CRED_OPTIONS = { 'description': app_cred_description, 'expires_at': app_cred_expires_str, 'unrestricted': False, - 'secret': app_cred_secret + 'secret': app_cred_secret, + 'access_rules': None, +} +ACCESS_RULE = { + 'id': access_rule_id, + 'service': access_rule_service, + 'path': access_rule_path, + 'method': access_rule_method, +} +APP_CRED_ACCESS_RULES = { + 'id': app_cred_id, + 'name': app_cred_name, + 'project_id': project_id, + 'roles': app_cred_role, + 'description': None, + 'expires_at': None, + 'unrestricted': False, + 'secret': app_cred_secret, + 'access_rules': app_cred_access_rules } registered_limit_id = 'registered-limit-id' @@ -625,6 +652,8 @@ class FakeIdentityv3Client(object): self.application_credentials = mock.Mock() self.application_credentials.resource_class = fakes.FakeResource(None, {}) + self.access_rules = mock.Mock() + self.access_rules.resource_class = fakes.FakeResource(None, {}) self.inference_rules = mock.Mock() self.inference_rules.resource_class = fakes.FakeResource(None, {}) self.registered_limits = mock.Mock() diff --git a/openstackclient/tests/unit/identity/v3/test_access_rule.py b/openstackclient/tests/unit/identity/v3/test_access_rule.py new file mode 100644 index 00000000..f8b6093a --- /dev/null +++ b/openstackclient/tests/unit/identity/v3/test_access_rule.py @@ -0,0 +1,174 @@ +# Copyright 2019 SUSE LLC +# +# 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 osc_lib import exceptions +from osc_lib import utils + +from openstackclient.identity.v3 import access_rule +from openstackclient.tests.unit import fakes +from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes + + +class TestAccessRule(identity_fakes.TestIdentityv3): + + def setUp(self): + super(TestAccessRule, self).setUp() + + identity_manager = self.app.client_manager.identity + self.access_rules_mock = identity_manager.access_rules + self.access_rules_mock.reset_mock() + self.roles_mock = identity_manager.roles + self.roles_mock.reset_mock() + + +class TestAccessRuleDelete(TestAccessRule): + + def setUp(self): + super(TestAccessRuleDelete, self).setUp() + + # This is the return value for utils.find_resource() + self.access_rules_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.ACCESS_RULE), + loaded=True, + ) + self.access_rules_mock.delete.return_value = None + + # Get the command object to test + self.cmd = access_rule.DeleteAccessRule( + self.app, None) + + def test_access_rule_delete(self): + arglist = [ + identity_fakes.access_rule_id, + ] + verifylist = [ + ('access_rule', [identity_fakes.access_rule_id]) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.access_rules_mock.delete.assert_called_with( + identity_fakes.access_rule_id, + ) + self.assertIsNone(result) + + @mock.patch.object(utils, 'find_resource') + def test_delete_multi_access_rules_with_exception(self, find_mock): + find_mock.side_effect = [self.access_rules_mock.get.return_value, + exceptions.CommandError] + arglist = [ + identity_fakes.access_rule_id, + 'nonexistent_access_rule', + ] + verifylist = [ + ('access_rule', arglist), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + try: + self.cmd.take_action(parsed_args) + self.fail('CommandError should be raised.') + except exceptions.CommandError as e: + self.assertEqual('1 of 2 access rules failed to' + ' delete.', str(e)) + + find_mock.assert_any_call(self.access_rules_mock, + identity_fakes.access_rule_id) + find_mock.assert_any_call(self.access_rules_mock, + 'nonexistent_access_rule') + + self.assertEqual(2, find_mock.call_count) + self.access_rules_mock.delete.assert_called_once_with( + identity_fakes.access_rule_id) + + +class TestAccessRuleList(TestAccessRule): + + def setUp(self): + super(TestAccessRuleList, self).setUp() + + self.access_rules_mock.list.return_value = [ + fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.ACCESS_RULE), + loaded=True, + ), + ] + + # Get the command object to test + self.cmd = access_rule.ListAccessRule(self.app, None) + + def test_access_rule_list(self): + arglist = [] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.access_rules_mock.list.assert_called_with(user=None) + + collist = ('ID', 'Service', 'Method', 'Path') + self.assertEqual(collist, columns) + datalist = (( + identity_fakes.access_rule_id, + identity_fakes.access_rule_service, + identity_fakes.access_rule_method, + identity_fakes.access_rule_path, + ), ) + self.assertEqual(datalist, tuple(data)) + + +class TestAccessRuleShow(TestAccessRule): + + def setUp(self): + super(TestAccessRuleShow, self).setUp() + + self.access_rules_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.ACCESS_RULE), + loaded=True, + ) + + # Get the command object to test + self.cmd = access_rule.ShowAccessRule(self.app, None) + + def test_access_rule_show(self): + arglist = [ + identity_fakes.access_rule_id, + ] + verifylist = [ + ('access_rule', identity_fakes.access_rule_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.access_rules_mock.get.assert_called_with( + identity_fakes.access_rule_id) + + collist = ('id', 'method', 'path', 'service') + self.assertEqual(collist, columns) + datalist = ( + identity_fakes.access_rule_id, + identity_fakes.access_rule_method, + identity_fakes.access_rule_path, + identity_fakes.access_rule_service, + ) + self.assertEqual(datalist, data) diff --git a/openstackclient/tests/unit/identity/v3/test_application_credential.py b/openstackclient/tests/unit/identity/v3/test_application_credential.py index 163aae9d..24bafc9f 100644 --- a/openstackclient/tests/unit/identity/v3/test_application_credential.py +++ b/openstackclient/tests/unit/identity/v3/test_application_credential.py @@ -14,6 +14,7 @@ # import copy +import json from unittest import mock from osc_lib import exceptions @@ -79,18 +80,20 @@ class TestApplicationCredentialCreate(TestApplicationCredential): 'expires_at': None, 'description': None, 'unrestricted': False, + 'access_rules': None, } self.app_creds_mock.create.assert_called_with( name, **kwargs ) - collist = ('description', 'expires_at', 'id', 'name', 'project_id', - 'roles', 'secret', 'unrestricted') + collist = ('access_rules', 'description', 'expires_at', 'id', 'name', + 'project_id', 'roles', 'secret', 'unrestricted') self.assertEqual(collist, columns) datalist = ( None, None, + None, identity_fakes.app_cred_id, identity_fakes.app_cred_name, identity_fakes.project_id, @@ -135,17 +138,19 @@ class TestApplicationCredentialCreate(TestApplicationCredential): 'roles': [identity_fakes.role_id], 'expires_at': identity_fakes.app_cred_expires, 'description': 'credential for testing', - 'unrestricted': False + 'unrestricted': False, + 'access_rules': None, } self.app_creds_mock.create.assert_called_with( name, **kwargs ) - collist = ('description', 'expires_at', 'id', 'name', 'project_id', - 'roles', 'secret', 'unrestricted') + collist = ('access_rules', 'description', 'expires_at', 'id', 'name', + 'project_id', 'roles', 'secret', 'unrestricted') self.assertEqual(collist, columns) datalist = ( + None, identity_fakes.app_cred_description, identity_fakes.app_cred_expires_str, identity_fakes.app_cred_id, @@ -157,6 +162,111 @@ class TestApplicationCredentialCreate(TestApplicationCredential): ) self.assertEqual(datalist, data) + def test_application_credential_create_with_access_rules_string(self): + name = identity_fakes.app_cred_name + self.app_creds_mock.create.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.APP_CRED_ACCESS_RULES), + loaded=True, + ) + + arglist = [ + name, + '--access-rules', identity_fakes.app_cred_access_rules, + ] + verifylist = [ + ('name', identity_fakes.app_cred_name), + ('access_rules', identity_fakes.app_cred_access_rules), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'secret': None, + 'roles': [], + 'expires_at': None, + 'description': None, + 'unrestricted': False, + 'access_rules': json.loads(identity_fakes.app_cred_access_rules) + } + self.app_creds_mock.create.assert_called_with( + name, + **kwargs + ) + + collist = ('access_rules', 'description', 'expires_at', 'id', 'name', + 'project_id', 'roles', 'secret', 'unrestricted') + self.assertEqual(collist, columns) + datalist = ( + identity_fakes.app_cred_access_rules, + None, + None, + identity_fakes.app_cred_id, + identity_fakes.app_cred_name, + identity_fakes.project_id, + identity_fakes.role_name, + identity_fakes.app_cred_secret, + False, + ) + self.assertEqual(datalist, data) + + @mock.patch('openstackclient.identity.v3.application_credential.json.load') + @mock.patch('openstackclient.identity.v3.application_credential.open') + def test_application_credential_create_with_access_rules_file( + self, _, mock_json_load): + mock_json_load.return_value = identity_fakes.app_cred_access_rules + + name = identity_fakes.app_cred_name + self.app_creds_mock.create.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.APP_CRED_ACCESS_RULES), + loaded=True, + ) + + arglist = [ + name, + '--access-rules', identity_fakes.app_cred_access_rules_path, + ] + verifylist = [ + ('name', identity_fakes.app_cred_name), + ('access_rules', identity_fakes.app_cred_access_rules_path), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'secret': None, + 'roles': [], + 'expires_at': None, + 'description': None, + 'unrestricted': False, + 'access_rules': identity_fakes.app_cred_access_rules + } + self.app_creds_mock.create.assert_called_with( + name, + **kwargs + ) + + collist = ('access_rules', 'description', 'expires_at', 'id', 'name', + 'project_id', 'roles', 'secret', 'unrestricted') + self.assertEqual(collist, columns) + datalist = ( + identity_fakes.app_cred_access_rules, + None, + None, + identity_fakes.app_cred_id, + identity_fakes.app_cred_name, + identity_fakes.project_id, + identity_fakes.role_name, + identity_fakes.app_cred_secret, + False, + ) + self.assertEqual(datalist, data) + class TestApplicationCredentialDelete(TestApplicationCredential): @@ -293,12 +403,13 @@ class TestApplicationCredentialShow(TestApplicationCredential): self.app_creds_mock.get.assert_called_with(identity_fakes.app_cred_id) - collist = ('description', 'expires_at', 'id', 'name', 'project_id', - 'roles', 'secret', 'unrestricted') + collist = ('access_rules', 'description', 'expires_at', 'id', 'name', + 'project_id', 'roles', 'secret', 'unrestricted') self.assertEqual(collist, columns) datalist = ( None, None, + None, identity_fakes.app_cred_id, identity_fakes.app_cred_name, identity_fakes.project_id, |
