Skip to content

Commit 375964f

Browse files
committed
Add CRUD support for application credentials
Add support for creating, retrieving, and deleting application credentials. Application credentials do not support updates. In order to provide a positive user experience for the `--role` option, this patch also includes an improvement to the `identity.common._get_token_resource()` function that allows it to introspect the roles list within a token. This way there is no need to make a request to keystone to retrieve a role object, which would fail most of the time anyway due to keystone's default policy prohibiting unprivileged users from retrieving roles. bp application-credentials Change-Id: I29e03b72acd931305cbdac5a9ff666854d05c6d7
1 parent 1e30be9 commit 375964f

8 files changed

Lines changed: 834 additions & 0 deletions

File tree

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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)

openstackclient/identity/common.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,13 @@ def _get_token_resource(client, resource, parsed_name, parsed_domain=None):
101101
# user/project under different domain may has a same name
102102
if parsed_domain and parsed_domain not in obj['domain'].values():
103103
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
104111
return obj['id'] if obj['name'] == parsed_name else parsed_name
105112
# diaper defense in case parsing the token fails
106113
except Exception: # noqa
Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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)))

0 commit comments

Comments
 (0)