Browse Source

Merge "Add CRUD support for application credentials"

tags/3.15.0
Zuul 1 year ago
parent
commit
e78c9bc00d

+ 109
- 0
doc/source/cli/command-objects/application-credentials.rst View File

@@ -0,0 +1,109 @@
1
+======================
2
+application credential
3
+======================
4
+
5
+Identity v3
6
+
7
+With application credentials, a user can grant their applications limited
8
+access to their cloud resources. Once created, users can authenticate with an
9
+application credential by using the ``v3applicationcredential`` auth type.
10
+
11
+application credential create
12
+-----------------------------
13
+
14
+Create new application credential
15
+
16
+.. program:: application credential create
17
+.. code:: bash
18
+
19
+    openstack application credential create
20
+        [--secret <secret>]
21
+        [--role <role>]
22
+        [--expiration <expiration>]
23
+        [--description <description>]
24
+        [--unrestricted]
25
+        <name>
26
+
27
+.. option:: --secret <secret>
28
+
29
+    Secret to use for authentication (if not provided, one will be generated)
30
+
31
+.. option:: --role <role>
32
+
33
+    Roles to authorize (name or ID) (repeat option to set multiple values)
34
+
35
+.. option:: --expiration <expiration>
36
+
37
+    Sets an expiration date for the application credential (format of
38
+    YYYY-mm-ddTHH:MM:SS)
39
+
40
+.. option:: --description <description>
41
+
42
+    Application credential description
43
+
44
+.. option:: --unrestricted
45
+
46
+    Enable application credential to create and delete other application
47
+    credentials and trusts (this is potentially dangerous behavior and is
48
+    disabled by default)
49
+
50
+.. option:: --restricted
51
+
52
+    Prohibit application credential from creating and deleting other
53
+    application credentials and trusts (this is the default behavior)
54
+
55
+.. describe:: <name>
56
+
57
+    Name of the application credential
58
+
59
+
60
+application credential delete
61
+-----------------------------
62
+
63
+Delete application credential(s)
64
+
65
+.. program:: application credential delete
66
+.. code:: bash
67
+
68
+    openstack application credential delete
69
+        <application-credential> [<application-credential> ...]
70
+
71
+.. describe:: <application-credential>
72
+
73
+    Application credential(s) to delete (name or ID)
74
+
75
+application credential list
76
+---------------------------
77
+
78
+List application credentials
79
+
80
+.. program:: application credential list
81
+.. code:: bash
82
+
83
+    openstack application credential list
84
+        [--user <user>]
85
+        [--user-domain <user-domain>]
86
+
87
+.. option:: --user
88
+
89
+    User whose application credentials to list (name or ID)
90
+
91
+.. option:: --user-domain
92
+
93
+    Domain the user belongs to (name or ID). This can be
94
+    used in case collisions between user names exist.
95
+
96
+application credential show
97
+---------------------------
98
+
99
+Display application credential details
100
+
101
+.. program:: application credential show
102
+.. code:: bash
103
+
104
+    openstack application credential show
105
+        <application-credential>
106
+
107
+.. describe:: <application-credential>
108
+
109
+    Application credential to display (name or ID)

+ 7
- 0
openstackclient/identity/common.py View File

@@ -101,6 +101,13 @@ def _get_token_resource(client, resource, parsed_name, parsed_domain=None):
101 101
         # user/project under different domain may has a same name
102 102
         if parsed_domain and parsed_domain not in obj['domain'].values():
103 103
             return parsed_name
104
+        if isinstance(obj, list):
105
+            for item in obj:
106
+                if item['name'] == parsed_name:
107
+                    return item['id']
108
+                if item['id'] == parsed_name:
109
+                    return parsed_name
110
+            return parsed_name
104 111
         return obj['id'] if obj['name'] == parsed_name else parsed_name
105 112
     # diaper defense in case parsing the token fails
106 113
     except Exception:  # noqa

+ 220
- 0
openstackclient/identity/v3/application_credential.py View File

@@ -0,0 +1,220 @@
1
+#   Copyright 2018 SUSE Linux GmbH
2
+#
3
+#   Licensed under the Apache License, Version 2.0 (the "License"); you may
4
+#   not use this file except in compliance with the License. You may obtain
5
+#   a copy of the License at
6
+#
7
+#        http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+#   Unless required by applicable law or agreed to in writing, software
10
+#   distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11
+#   WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12
+#   License for the specific language governing permissions and limitations
13
+#   under the License.
14
+#
15
+
16
+"""Identity v3 Application Credential action implementations"""
17
+
18
+import datetime
19
+import logging
20
+
21
+from osc_lib.command import command
22
+from osc_lib import exceptions
23
+from osc_lib import utils
24
+import six
25
+
26
+from openstackclient.i18n import _
27
+from openstackclient.identity import common
28
+
29
+
30
+LOG = logging.getLogger(__name__)
31
+
32
+
33
+class CreateApplicationCredential(command.ShowOne):
34
+    _description = _("Create new application credential")
35
+
36
+    def get_parser(self, prog_name):
37
+        parser = super(CreateApplicationCredential, self).get_parser(prog_name)
38
+        parser.add_argument(
39
+            'name',
40
+            metavar='<name>',
41
+            help=_('Name of the application credential'),
42
+        )
43
+        parser.add_argument(
44
+            '--secret',
45
+            metavar='<secret>',
46
+            help=_('Secret to use for authentication (if not provided, one'
47
+                   ' will be generated)'),
48
+        )
49
+        parser.add_argument(
50
+            '--role',
51
+            metavar='<role>',
52
+            action='append',
53
+            default=[],
54
+            help=_('Roles to authorize (name or ID) (repeat option to set'
55
+                   ' multiple values)'),
56
+        )
57
+        parser.add_argument(
58
+            '--expiration',
59
+            metavar='<expiration>',
60
+            help=_('Sets an expiration date for the application credential,'
61
+                   ' format of YYYY-mm-ddTHH:MM:SS (if not provided, the'
62
+                   ' application credential will not expire)'),
63
+        )
64
+        parser.add_argument(
65
+            '--description',
66
+            metavar='<description>',
67
+            help=_('Application credential description'),
68
+        )
69
+        parser.add_argument(
70
+            '--unrestricted',
71
+            action="store_true",
72
+            help=_('Enable application credential to create and delete other'
73
+                   ' application credentials and trusts (this is potentially'
74
+                   ' dangerous behavior and is disabled by default)'),
75
+        )
76
+        parser.add_argument(
77
+            '--restricted',
78
+            action="store_true",
79
+            help=_('Prohibit application credential from creating and deleting'
80
+                   ' other application credentials and trusts (this is the'
81
+                   ' default behavior)'),
82
+        )
83
+        return parser
84
+
85
+    def take_action(self, parsed_args):
86
+        identity_client = self.app.client_manager.identity
87
+
88
+        role_ids = []
89
+        for role in parsed_args.role:
90
+            # A user can only create an application credential for themself,
91
+            # not for another user even as an admin, and only on the project to
92
+            # which they are currently scoped with a subset of the role
93
+            # assignments they have on that project. Don't bother trying to
94
+            # look up roles via keystone, just introspect the token.
95
+            role_id = common._get_token_resource(identity_client, "roles",
96
+                                                 role)
97
+            role_ids.append(role_id)
98
+
99
+        expires_at = None
100
+        if parsed_args.expiration:
101
+            expires_at = datetime.datetime.strptime(parsed_args.expiration,
102
+                                                    '%Y-%m-%dT%H:%M:%S')
103
+
104
+        if parsed_args.restricted:
105
+            unrestricted = False
106
+        else:
107
+            unrestricted = parsed_args.unrestricted
108
+
109
+        app_cred_manager = identity_client.application_credentials
110
+        application_credential = app_cred_manager.create(
111
+            parsed_args.name,
112
+            roles=role_ids,
113
+            expires_at=expires_at,
114
+            description=parsed_args.description,
115
+            secret=parsed_args.secret,
116
+            unrestricted=unrestricted,
117
+        )
118
+
119
+        application_credential._info.pop('links', None)
120
+
121
+        # Format roles into something sensible
122
+        roles = application_credential._info.pop('roles')
123
+        msg = ' '.join(r['name'] for r in roles)
124
+        application_credential._info['roles'] = msg
125
+
126
+        return zip(*sorted(six.iteritems(application_credential._info)))
127
+
128
+
129
+class DeleteApplicationCredential(command.Command):
130
+    _description = _("Delete application credentials(s)")
131
+
132
+    def get_parser(self, prog_name):
133
+        parser = super(DeleteApplicationCredential, self).get_parser(prog_name)
134
+        parser.add_argument(
135
+            'application_credential',
136
+            metavar='<application-credential>',
137
+            nargs="+",
138
+            help=_('Application credentials(s) to delete (name or ID)'),
139
+        )
140
+        return parser
141
+
142
+    def take_action(self, parsed_args):
143
+        identity_client = self.app.client_manager.identity
144
+
145
+        errors = 0
146
+        for ac in parsed_args.application_credential:
147
+            try:
148
+                app_cred = utils.find_resource(
149
+                    identity_client.application_credentials, ac)
150
+                identity_client.application_credentials.delete(app_cred.id)
151
+            except Exception as e:
152
+                errors += 1
153
+                LOG.error(_("Failed to delete application credential with "
154
+                          "name or ID '%(ac)s': %(e)s"),
155
+                          {'ac': ac, 'e': e})
156
+
157
+        if errors > 0:
158
+            total = len(parsed_args.application_credential)
159
+            msg = (_("%(errors)s of %(total)s application credentials failed "
160
+                   "to delete.") % {'errors': errors, 'total': total})
161
+            raise exceptions.CommandError(msg)
162
+
163
+
164
+class ListApplicationCredential(command.Lister):
165
+    _description = _("List application credentials")
166
+
167
+    def get_parser(self, prog_name):
168
+        parser = super(ListApplicationCredential, self).get_parser(prog_name)
169
+        parser.add_argument(
170
+            '--user',
171
+            metavar='<user>',
172
+            help=_('User whose application credentials to list (name or ID)'),
173
+        )
174
+        common.add_user_domain_option_to_parser(parser)
175
+        return parser
176
+
177
+    def take_action(self, parsed_args):
178
+        identity_client = self.app.client_manager.identity
179
+        if parsed_args.user:
180
+            user_id = common.find_user(identity_client,
181
+                                       parsed_args.user,
182
+                                       parsed_args.user_domain).id
183
+        else:
184
+            user_id = None
185
+
186
+        columns = ('ID', 'Name', 'Project ID', 'Description', 'Expires At')
187
+        data = identity_client.application_credentials.list(
188
+            user=user_id)
189
+        return (columns,
190
+                (utils.get_item_properties(
191
+                    s, columns,
192
+                    formatters={},
193
+                ) for s in data))
194
+
195
+
196
+class ShowApplicationCredential(command.ShowOne):
197
+    _description = _("Display application credential details")
198
+
199
+    def get_parser(self, prog_name):
200
+        parser = super(ShowApplicationCredential, self).get_parser(prog_name)
201
+        parser.add_argument(
202
+            'application_credential',
203
+            metavar='<application-credential>',
204
+            help=_('Application credential to display (name or ID)'),
205
+        )
206
+        return parser
207
+
208
+    def take_action(self, parsed_args):
209
+        identity_client = self.app.client_manager.identity
210
+        app_cred = utils.find_resource(identity_client.application_credentials,
211
+                                       parsed_args.application_credential)
212
+
213
+        app_cred._info.pop('links', None)
214
+
215
+        # Format roles into something sensible
216
+        roles = app_cred._info.pop('roles')
217
+        msg = ' '.join(r['name'] for r in roles)
218
+        app_cred._info['roles'] = msg
219
+
220
+        return zip(*sorted(six.iteritems(app_cred._info)))

+ 143
- 0
openstackclient/tests/functional/identity/v3/test_application_credential.py View File

@@ -0,0 +1,143 @@
1
+#    Copyright 2018 SUSE Linux GmbH
2
+#
3
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
4
+#    not use this file except in compliance with the License. You may obtain
5
+#    a copy of the License at
6
+#
7
+#         http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+#    Unless required by applicable law or agreed to in writing, software
10
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12
+#    License for the specific language governing permissions and limitations
13
+#    under the License.
14
+
15
+import datetime
16
+
17
+from tempest.lib.common.utils import data_utils
18
+
19
+from openstackclient.tests.functional.identity.v3 import common
20
+
21
+
22
+class ApplicationCredentialTests(common.IdentityTests):
23
+
24
+    APPLICATION_CREDENTIAL_FIELDS = ['id', 'name', 'project_id',
25
+                                     'description', 'roles', 'expires_at',
26
+                                     'unrestricted']
27
+    APPLICATION_CREDENTIAL_LIST_HEADERS = ['ID', 'Name', 'Project ID',
28
+                                           'Description', 'Expires At']
29
+
30
+    def test_application_credential_create(self):
31
+        name = data_utils.rand_name('name')
32
+        raw_output = self.openstack('application credential create %(name)s'
33
+                                    % {'name': name})
34
+        self.addCleanup(
35
+            self.openstack,
36
+            'application credential delete %(name)s' % {'name': name})
37
+        items = self.parse_show(raw_output)
38
+        self.assert_show_fields(items, self.APPLICATION_CREDENTIAL_FIELDS)
39
+
40
+    def _create_role_assignments(self):
41
+        try:
42
+            user = self.openstack('configuration show -f value'
43
+                                  ' -c auth.username')
44
+        except Exception:
45
+            user = self.openstack('configuration show -f value'
46
+                                  ' -c auth.user_id')
47
+        try:
48
+            user_domain = self.openstack('configuration show -f value'
49
+                                         ' -c auth.user_domain_name')
50
+        except Exception:
51
+            user_domain = self.openstack('configuration show -f value'
52
+                                         ' -c auth.user_domain_id')
53
+        try:
54
+            project = self.openstack('configuration show -f value'
55
+                                     ' -c auth.project_name')
56
+        except Exception:
57
+            project = self.openstack('configuration show -f value'
58
+                                     ' -c auth.project_id')
59
+        try:
60
+            project_domain = self.openstack('configuration show -f value'
61
+                                            ' -c auth.project_domain_name')
62
+        except Exception:
63
+            project_domain = self.openstack('configuration show -f value'
64
+                                            ' -c auth.project_domain_id')
65
+        role1 = self._create_dummy_role()
66
+        role2 = self._create_dummy_role()
67
+        for role in role1, role2:
68
+            self.openstack('role add'
69
+                           ' --user %(user)s'
70
+                           ' --user-domain %(user_domain)s'
71
+                           ' --project %(project)s'
72
+                           ' --project-domain %(project_domain)s'
73
+                           ' %(role)s'
74
+                           % {'user': user,
75
+                              'user_domain': user_domain,
76
+                              'project': project,
77
+                              'project_domain': project_domain,
78
+                              'role': role})
79
+            self.addCleanup(self.openstack,
80
+                            'role remove'
81
+                            ' --user %(user)s'
82
+                            ' --user-domain %(user_domain)s'
83
+                            ' --project %(project)s'
84
+                            ' --project-domain %(project_domain)s'
85
+                            ' %(role)s'
86
+                            % {'user': user,
87
+                               'user_domain': user_domain,
88
+                               'project': project,
89
+                               'project_domain': project_domain,
90
+                               'role': role})
91
+        return role1, role2
92
+
93
+    def test_application_credential_create_with_options(self):
94
+        name = data_utils.rand_name('name')
95
+        secret = data_utils.rand_name('secret')
96
+        description = data_utils.rand_name('description')
97
+        tomorrow = (datetime.datetime.utcnow() +
98
+                    datetime.timedelta(days=1)).strftime('%Y-%m-%dT%H:%M:%S%z')
99
+        role1, role2 = self._create_role_assignments()
100
+        raw_output = self.openstack('application credential create %(name)s'
101
+                                    ' --secret %(secret)s'
102
+                                    ' --description %(description)s'
103
+                                    ' --expiration %(tomorrow)s'
104
+                                    ' --role %(role1)s'
105
+                                    ' --role %(role2)s'
106
+                                    ' --unrestricted'
107
+                                    % {'name': name,
108
+                                       'secret': secret,
109
+                                       'description': description,
110
+                                       'tomorrow': tomorrow,
111
+                                       'role1': role1,
112
+                                       'role2': role2})
113
+        self.addCleanup(
114
+            self.openstack,
115
+            'application credential delete %(name)s' % {'name': name})
116
+        items = self.parse_show(raw_output)
117
+        self.assert_show_fields(items, self.APPLICATION_CREDENTIAL_FIELDS)
118
+
119
+    def test_application_credential_delete(self):
120
+        name = data_utils.rand_name('name')
121
+        self.openstack('application credential create %(name)s'
122
+                       % {'name': name})
123
+        raw_output = self.openstack('application credential delete '
124
+                                    '%(name)s' % {'name': name})
125
+        self.assertEqual(0, len(raw_output))
126
+
127
+    def test_application_credential_list(self):
128
+        raw_output = self.openstack('application credential list')
129
+        items = self.parse_listing(raw_output)
130
+        self.assert_table_structure(
131
+            items, self.APPLICATION_CREDENTIAL_LIST_HEADERS)
132
+
133
+    def test_application_credential_show(self):
134
+        name = data_utils.rand_name('name')
135
+        raw_output = self.openstack('application credential create %(name)s'
136
+                                    % {'name': name})
137
+        self.addCleanup(
138
+            self.openstack,
139
+            'application credential delete %(name)s' % {'name': name})
140
+        raw_output = self.openstack('application credential show '
141
+                                    '%(name)s' % {'name': name})
142
+        items = self.parse_show(raw_output)
143
+        self.assert_show_fields(items, self.APPLICATION_CREDENTIAL_FIELDS)

+ 32
- 0
openstackclient/tests/unit/identity/v3/fakes.py View File

@@ -14,6 +14,7 @@
14 14
 #
15 15
 
16 16
 import copy
17
+import datetime
17 18
 import uuid
18 19
 
19 20
 from keystoneauth1 import access
@@ -457,6 +458,34 @@ OAUTH_VERIFIER = {
457 458
     'oauth_verifier': oauth_verifier_pin
458 459
 }
459 460
 
461
+app_cred_id = 'app-cred-id'
462
+app_cred_name = 'testing_app_cred'
463
+app_cred_role = {"id": role_id, "name": role_name, "domain": None},
464
+app_cred_description = 'app credential for testing'
465
+app_cred_expires = datetime.datetime(2022, 1, 1, 0, 0)
466
+app_cred_expires_str = app_cred_expires.strftime('%Y-%m-%dT%H:%M:%S%z')
467
+app_cred_secret = 'moresecuresecret'
468
+APP_CRED_BASIC = {
469
+    'id': app_cred_id,
470
+    'name': app_cred_name,
471
+    'project_id': project_id,
472
+    'roles': app_cred_role,
473
+    'description': None,
474
+    'expires_at': None,
475
+    'unrestricted': False,
476
+    'secret': app_cred_secret
477
+}
478
+APP_CRED_OPTIONS = {
479
+    'id': app_cred_id,
480
+    'name': app_cred_name,
481
+    'project_id': project_id,
482
+    'roles': app_cred_role,
483
+    'description': app_cred_description,
484
+    'expires_at': app_cred_expires_str,
485
+    'unrestricted': False,
486
+    'secret': app_cred_secret
487
+}
488
+
460 489
 
461 490
 def fake_auth_ref(fake_token, fake_service=None):
462 491
     """Create an auth_ref using keystoneauth's fixtures"""
@@ -544,6 +573,9 @@ class FakeIdentityv3Client(object):
544 573
         self.auth = FakeAuth()
545 574
         self.auth.client = mock.Mock()
546 575
         self.auth.client.resource_class = fakes.FakeResource(None, {})
576
+        self.application_credentials = mock.Mock()
577
+        self.application_credentials.resource_class = fakes.FakeResource(None,
578
+                                                                         {})
547 579
 
548 580
 
549 581
 class FakeFederationManager(object):

+ 309
- 0
openstackclient/tests/unit/identity/v3/test_application_credential.py View File

@@ -0,0 +1,309 @@
1
+#   Copyright 2018 SUSE Linux GmbH
2
+#
3
+#   Licensed under the Apache License, Version 2.0 (the "License"); you may
4
+#   not use this file except in compliance with the License. You may obtain
5
+#   a copy of the License at
6
+#
7
+#        http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+#   Unless required by applicable law or agreed to in writing, software
10
+#   distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11
+#   WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12
+#   License for the specific language governing permissions and limitations
13
+#   under the License.
14
+#
15
+
16
+import copy
17
+
18
+import mock
19
+from osc_lib import exceptions
20
+from osc_lib import utils
21
+
22
+from openstackclient.identity.v3 import application_credential
23
+from openstackclient.tests.unit import fakes
24
+from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes
25
+
26
+
27
+class TestApplicationCredential(identity_fakes.TestIdentityv3):
28
+
29
+    def setUp(self):
30
+        super(TestApplicationCredential, self).setUp()
31
+
32
+        identity_manager = self.app.client_manager.identity
33
+        self.app_creds_mock = identity_manager.application_credentials
34
+        self.app_creds_mock.reset_mock()
35
+        self.roles_mock = identity_manager.roles
36
+        self.roles_mock.reset_mock()
37
+
38
+
39
+class TestApplicationCredentialCreate(TestApplicationCredential):
40
+
41
+    def setUp(self):
42
+        super(TestApplicationCredentialCreate, self).setUp()
43
+
44
+        self.roles_mock.get.return_value = fakes.FakeResource(
45
+            None,
46
+            copy.deepcopy(identity_fakes.ROLE),
47
+            loaded=True,
48
+        )
49
+
50
+        # Get the command object to test
51
+        self.cmd = application_credential.CreateApplicationCredential(
52
+            self.app, None)
53
+
54
+    def test_application_credential_create_basic(self):
55
+        self.app_creds_mock.create.return_value = fakes.FakeResource(
56
+            None,
57
+            copy.deepcopy(identity_fakes.APP_CRED_BASIC),
58
+            loaded=True,
59
+        )
60
+
61
+        name = identity_fakes.app_cred_name
62
+        arglist = [
63
+            name
64
+        ]
65
+        verifylist = [
66
+            ('name', identity_fakes.app_cred_name)
67
+        ]
68
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
69
+
70
+        # In base command class ShowOne in cliff, abstract method take_action()
71
+        # returns a two-part tuple with a tuple of column names and a tuple of
72
+        # data to be shown.
73
+        columns, data = self.cmd.take_action(parsed_args)
74
+
75
+        # Set expected values
76
+        kwargs = {
77
+            'secret': None,
78
+            'roles': [],
79
+            'expires_at': None,
80
+            'description': None,
81
+            'unrestricted': False,
82
+        }
83
+        self.app_creds_mock.create.assert_called_with(
84
+            name,
85
+            **kwargs
86
+        )
87
+
88
+        collist = ('description', 'expires_at', 'id', 'name', 'project_id',
89
+                   'roles', 'secret', 'unrestricted')
90
+        self.assertEqual(collist, columns)
91
+        datalist = (
92
+            None,
93
+            None,
94
+            identity_fakes.app_cred_id,
95
+            identity_fakes.app_cred_name,
96
+            identity_fakes.project_id,
97
+            identity_fakes.role_name,
98
+            identity_fakes.app_cred_secret,
99
+            False,
100
+        )
101
+        self.assertEqual(datalist, data)
102
+
103
+    def test_application_credential_create_with_options(self):
104
+        name = identity_fakes.app_cred_name
105
+        self.app_creds_mock.create.return_value = fakes.FakeResource(
106
+            None,
107
+            copy.deepcopy(identity_fakes.APP_CRED_OPTIONS),
108
+            loaded=True,
109
+        )
110
+
111
+        arglist = [
112
+            name,
113
+            '--secret', 'moresecuresecret',
114
+            '--role', identity_fakes.role_id,
115
+            '--expiration', identity_fakes.app_cred_expires_str,
116
+            '--description', 'credential for testing'
117
+        ]
118
+        verifylist = [
119
+            ('name', identity_fakes.app_cred_name),
120
+            ('secret', 'moresecuresecret'),
121
+            ('role', [identity_fakes.role_id]),
122
+            ('expiration', identity_fakes.app_cred_expires_str),
123
+            ('description', 'credential for testing')
124
+        ]
125
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
126
+
127
+        # In base command class ShowOne in cliff, abstract method take_action()
128
+        # returns a two-part tuple with a tuple of column names and a tuple of
129
+        # data to be shown.
130
+        columns, data = self.cmd.take_action(parsed_args)
131
+
132
+        # Set expected values
133
+        kwargs = {
134
+            'secret': 'moresecuresecret',
135
+            'roles': [identity_fakes.role_id],
136
+            'expires_at': identity_fakes.app_cred_expires,
137
+            'description': 'credential for testing',
138
+            'unrestricted': False
139
+        }
140
+        self.app_creds_mock.create.assert_called_with(
141
+            name,
142
+            **kwargs
143
+        )
144
+
145
+        collist = ('description', 'expires_at', 'id', 'name', 'project_id',
146
+                   'roles', 'secret', 'unrestricted')
147
+        self.assertEqual(collist, columns)
148
+        datalist = (
149
+            identity_fakes.app_cred_description,
150
+            identity_fakes.app_cred_expires_str,
151
+            identity_fakes.app_cred_id,
152
+            identity_fakes.app_cred_name,
153
+            identity_fakes.project_id,
154
+            identity_fakes.role_name,
155
+            identity_fakes.app_cred_secret,
156
+            False,
157
+        )
158
+        self.assertEqual(datalist, data)
159
+
160
+
161
+class TestApplicationCredentialDelete(TestApplicationCredential):
162
+
163
+    def setUp(self):
164
+        super(TestApplicationCredentialDelete, self).setUp()
165
+
166
+        # This is the return value for utils.find_resource()
167
+        self.app_creds_mock.get.return_value = fakes.FakeResource(
168
+            None,
169
+            copy.deepcopy(identity_fakes.APP_CRED_BASIC),
170
+            loaded=True,
171
+        )
172
+        self.app_creds_mock.delete.return_value = None
173
+
174
+        # Get the command object to test
175
+        self.cmd = application_credential.DeleteApplicationCredential(
176
+            self.app, None)
177
+
178
+    def test_application_credential_delete(self):
179
+        arglist = [
180
+            identity_fakes.app_cred_id,
181
+        ]
182
+        verifylist = [
183
+            ('application_credential', [identity_fakes.app_cred_id])
184
+        ]
185
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
186
+
187
+        result = self.cmd.take_action(parsed_args)
188
+
189
+        self.app_creds_mock.delete.assert_called_with(
190
+            identity_fakes.app_cred_id,
191
+        )
192
+        self.assertIsNone(result)
193
+
194
+    @mock.patch.object(utils, 'find_resource')
195
+    def test_delete_multi_app_creds_with_exception(self, find_mock):
196
+        find_mock.side_effect = [self.app_creds_mock.get.return_value,
197
+                                 exceptions.CommandError]
198
+        arglist = [
199
+            identity_fakes.app_cred_id,
200
+            'nonexistent_app_cred',
201
+        ]
202
+        verifylist = [
203
+            ('application_credential', arglist),
204
+        ]
205
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
206
+
207
+        try:
208
+            self.cmd.take_action(parsed_args)
209
+            self.fail('CommandError should be raised.')
210
+        except exceptions.CommandError as e:
211
+            self.assertEqual('1 of 2 application credentials failed to'
212
+                             ' delete.', str(e))
213
+
214
+        find_mock.assert_any_call(self.app_creds_mock,
215
+                                  identity_fakes.app_cred_id)
216
+        find_mock.assert_any_call(self.app_creds_mock,
217
+                                  'nonexistent_app_cred')
218
+
219
+        self.assertEqual(2, find_mock.call_count)
220
+        self.app_creds_mock.delete.assert_called_once_with(
221
+            identity_fakes.app_cred_id)
222
+
223
+
224
+class TestApplicationCredentialList(TestApplicationCredential):
225
+
226
+    def setUp(self):
227
+        super(TestApplicationCredentialList, self).setUp()
228
+
229
+        self.app_creds_mock.list.return_value = [
230
+            fakes.FakeResource(
231
+                None,
232
+                copy.deepcopy(identity_fakes.APP_CRED_BASIC),
233
+                loaded=True,
234
+            ),
235
+        ]
236
+
237
+        # Get the command object to test
238
+        self.cmd = application_credential.ListApplicationCredential(self.app,
239
+                                                                    None)
240
+
241
+    def test_application_credential_list(self):
242
+        arglist = []
243
+        verifylist = []
244
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
245
+
246
+        # In base command class Lister in cliff, abstract method take_action()
247
+        # returns a tuple containing the column names and an iterable
248
+        # containing the data to be listed.
249
+        columns, data = self.cmd.take_action(parsed_args)
250
+
251
+        self.app_creds_mock.list.assert_called_with(user=None)
252
+
253
+        collist = ('ID', 'Name', 'Project ID', 'Description', 'Expires At')
254
+        self.assertEqual(collist, columns)
255
+        datalist = ((
256
+            identity_fakes.app_cred_id,
257
+            identity_fakes.app_cred_name,
258
+            identity_fakes.project_id,
259
+            None,
260
+            None
261
+        ), )
262
+        self.assertEqual(datalist, tuple(data))
263
+
264
+
265
+class TestApplicationCredentialShow(TestApplicationCredential):
266
+
267
+    def setUp(self):
268
+        super(TestApplicationCredentialShow, self).setUp()
269
+
270
+        self.app_creds_mock.get.return_value = fakes.FakeResource(
271
+            None,
272
+            copy.deepcopy(identity_fakes.APP_CRED_BASIC),
273
+            loaded=True,
274
+        )
275
+
276
+        # Get the command object to test
277
+        self.cmd = application_credential.ShowApplicationCredential(self.app,
278
+                                                                    None)
279
+
280
+    def test_application_credential_show(self):
281
+        arglist = [
282
+            identity_fakes.app_cred_id,
283
+        ]
284
+        verifylist = [
285
+            ('application_credential', identity_fakes.app_cred_id),
286
+        ]
287
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
288
+
289
+        # In base command class ShowOne in cliff, abstract method take_action()
290
+        # returns a two-part tuple with a tuple of column names and a tuple of
291
+        # data to be shown.
292
+        columns, data = self.cmd.take_action(parsed_args)
293
+
294
+        self.app_creds_mock.get.assert_called_with(identity_fakes.app_cred_id)
295
+
296
+        collist = ('description', 'expires_at', 'id', 'name', 'project_id',
297
+                   'roles', 'secret', 'unrestricted')
298
+        self.assertEqual(collist, columns)
299
+        datalist = (
300
+            None,
301
+            None,
302
+            identity_fakes.app_cred_id,
303
+            identity_fakes.app_cred_name,
304
+            identity_fakes.project_id,
305
+            identity_fakes.role_name,
306
+            identity_fakes.app_cred_secret,
307
+            False,
308
+        )
309
+        self.assertEqual(datalist, data)

+ 9
- 0
releasenotes/notes/bp-application-credential-a7031a043efc4a25.yaml View File

@@ -0,0 +1,9 @@
1
+---
2
+features:
3
+  - |
4
+    Adds support for creating, reading, and deleting application credentials
5
+    via the ``appication credential`` command. With application credentials, a
6
+    user can grant their applications limited access to their cloud resources.
7
+    Once created, users can authenticate with an application credential by
8
+    using the ``v3applicationcredential`` auth type.
9
+    [`blueprint application-credentials <https://blueprints.launchpad.net/keystone/+spec/application-credentials>`_]

+ 5
- 0
setup.cfg View File

@@ -202,6 +202,11 @@ openstack.identity.v2 =
202 202
 openstack.identity.v3 =
203 203
     access_token_create = openstackclient.identity.v3.token:CreateAccessToken
204 204
 
205
+    application_credential_create = openstackclient.identity.v3.application_credential:CreateApplicationCredential
206
+    application_credential_delete = openstackclient.identity.v3.application_credential:DeleteApplicationCredential
207
+    application_credential_list = openstackclient.identity.v3.application_credential:ListApplicationCredential
208
+    application_credential_show = openstackclient.identity.v3.application_credential:ShowApplicationCredential
209
+
205 210
     catalog_list = openstackclient.identity.v3.catalog:ListCatalog
206 211
     catalog_show = openstackclient.identity.v3.catalog:ShowCatalog
207 212
 

Loading…
Cancel
Save