Skip to content

Commit 70ab3f9

Browse files
committed
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
1 parent db29e28 commit 70ab3f9

File tree

11 files changed

+548
-11
lines changed

11 files changed

+548
-11
lines changed
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
===========
2+
access rule
3+
===========
4+
5+
Identity v3
6+
7+
Access rules are fine-grained permissions for application credentials. An access
8+
rule comprises of a service type, a request path, and a request method. Access
9+
rules may only be created as attributes of application credentials, but they may
10+
be viewed and deleted independently.
11+
12+
13+
access rule delete
14+
------------------
15+
16+
Delete access rule(s)
17+
18+
.. program:: access rule delete
19+
.. code:: bash
20+
21+
openstack access rule delete <access-rule> [<access-rule> ...]
22+
23+
.. describe:: <access-rule>
24+
25+
Access rule(s) to delete (ID)
26+
27+
access rule list
28+
----------------
29+
30+
List access rules
31+
32+
.. program:: access rule list
33+
.. code:: bash
34+
35+
openstack access rule list
36+
[--user <user>]
37+
[--user-domain <user-domain>]
38+
39+
.. option:: --user
40+
41+
User whose access rules to list (name or ID). If not provided, looks up the
42+
current user's access rules.
43+
44+
.. option:: --user-domain
45+
46+
Domain the user belongs to (name or ID). This can be
47+
used in case collisions between user names exist.
48+
49+
access rule show
50+
---------------------------
51+
52+
Display access rule details
53+
54+
.. program:: access rule show
55+
.. code:: bash
56+
57+
openstack access rule show <access-rule>
58+
59+
.. describe:: <access-rule>
60+
61+
Access rule to display (ID)

doc/source/cli/command-objects/application-credentials.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ Create new application credential
2222
[--expiration <expiration>]
2323
[--description <description>]
2424
[--restricted|--unrestricted]
25+
[--access-rules <access-rules>]
2526
<name>
2627
2728
.. option:: --secret <secret>
@@ -52,6 +53,12 @@ Create new application credential
5253
Prohibit application credential from creating and deleting other
5354
application credentials and trusts (this is the default behavior)
5455

56+
.. option:: --access-rules
57+
58+
Either a string or file path containing a JSON-formatted list of access
59+
rules, each containing a request method, path, and service, for example
60+
'[{"method": "GET", "path": "/v2.1/servers", "service": "compute"}]'
61+
5562
.. describe:: <name>
5663

5764
Name of the application credential

lower-constraints.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ python-heatclient==1.10.0
9595
python-ironic-inspector-client==1.5.0
9696
python-ironicclient==2.3.0
9797
python-karborclient==0.6.0
98-
python-keystoneclient==3.17.0
98+
python-keystoneclient==3.22.0
9999
python-mimeparse==1.6.0
100100
python-mistralclient==3.1.0
101101
python-muranoclient==0.8.2
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
# Copyright 2019 SUSE LLC
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 Access Rule action implementations"""
17+
18+
import logging
19+
20+
from osc_lib.command import command
21+
from osc_lib import exceptions
22+
from osc_lib import utils
23+
import six
24+
25+
from openstackclient.i18n import _
26+
from openstackclient.identity import common
27+
28+
29+
LOG = logging.getLogger(__name__)
30+
31+
32+
class DeleteAccessRule(command.Command):
33+
_description = _("Delete access rule(s)")
34+
35+
def get_parser(self, prog_name):
36+
parser = super(DeleteAccessRule, self).get_parser(prog_name)
37+
parser.add_argument(
38+
'access_rule',
39+
metavar='<access-rule>',
40+
nargs="+",
41+
help=_('Application credentials(s) to delete (name or ID)'),
42+
)
43+
return parser
44+
45+
def take_action(self, parsed_args):
46+
identity_client = self.app.client_manager.identity
47+
48+
errors = 0
49+
for ac in parsed_args.access_rule:
50+
try:
51+
access_rule = utils.find_resource(
52+
identity_client.access_rules, ac)
53+
identity_client.access_rules.delete(access_rule.id)
54+
except Exception as e:
55+
errors += 1
56+
LOG.error(_("Failed to delete access rule with "
57+
"ID '%(ac)s': %(e)s"),
58+
{'ac': ac, 'e': e})
59+
60+
if errors > 0:
61+
total = len(parsed_args.access_rule)
62+
msg = (_("%(errors)s of %(total)s access rules failed "
63+
"to delete.") % {'errors': errors, 'total': total})
64+
raise exceptions.CommandError(msg)
65+
66+
67+
class ListAccessRule(command.Lister):
68+
_description = _("List access rules")
69+
70+
def get_parser(self, prog_name):
71+
parser = super(ListAccessRule, self).get_parser(prog_name)
72+
parser.add_argument(
73+
'--user',
74+
metavar='<user>',
75+
help=_('User whose access rules to list (name or ID)'),
76+
)
77+
common.add_user_domain_option_to_parser(parser)
78+
return parser
79+
80+
def take_action(self, parsed_args):
81+
identity_client = self.app.client_manager.identity
82+
if parsed_args.user:
83+
user_id = common.find_user(identity_client,
84+
parsed_args.user,
85+
parsed_args.user_domain).id
86+
else:
87+
user_id = None
88+
89+
columns = ('ID', 'Service', 'Method', 'Path')
90+
data = identity_client.access_rules.list(
91+
user=user_id)
92+
return (columns,
93+
(utils.get_item_properties(
94+
s, columns,
95+
formatters={},
96+
) for s in data))
97+
98+
99+
class ShowAccessRule(command.ShowOne):
100+
_description = _("Display access rule details")
101+
102+
def get_parser(self, prog_name):
103+
parser = super(ShowAccessRule, self).get_parser(prog_name)
104+
parser.add_argument(
105+
'access_rule',
106+
metavar='<access-rule>',
107+
help=_('Application credential to display (name or ID)'),
108+
)
109+
return parser
110+
111+
def take_action(self, parsed_args):
112+
identity_client = self.app.client_manager.identity
113+
access_rule = utils.find_resource(identity_client.access_rules,
114+
parsed_args.access_rule)
115+
116+
access_rule._info.pop('links', None)
117+
118+
return zip(*sorted(six.iteritems(access_rule._info)))

openstackclient/identity/v3/application_credential.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"""Identity v3 Application Credential action implementations"""
1717

1818
import datetime
19+
import json
1920
import logging
2021

2122
from osc_lib.command import command
@@ -79,6 +80,17 @@ def get_parser(self, prog_name):
7980
' other application credentials and trusts (this is the'
8081
' default behavior)'),
8182
)
83+
parser.add_argument(
84+
'--access-rules',
85+
metavar='<access-rules>',
86+
help=_('Either a string or file path containing a JSON-formatted '
87+
'list of access rules, each containing a request method, '
88+
'path, and service, for example '
89+
'\'[{"method": "GET", '
90+
'"path": "/v2.1/servers", '
91+
'"service": "compute"}]\''),
92+
93+
)
8294
return parser
8395

8496
def take_action(self, parsed_args):
@@ -105,6 +117,20 @@ def take_action(self, parsed_args):
105117
else:
106118
unrestricted = parsed_args.unrestricted
107119

120+
if parsed_args.access_rules:
121+
try:
122+
access_rules = json.loads(parsed_args.access_rules)
123+
except ValueError:
124+
try:
125+
with open(parsed_args.access_rules) as f:
126+
access_rules = json.load(f)
127+
except IOError:
128+
raise exceptions.CommandError(
129+
_("Access rules is not valid JSON string or file does"
130+
" not exist."))
131+
else:
132+
access_rules = None
133+
108134
app_cred_manager = identity_client.application_credentials
109135
application_credential = app_cred_manager.create(
110136
parsed_args.name,
@@ -113,6 +139,7 @@ def take_action(self, parsed_args):
113139
description=parsed_args.description,
114140
secret=parsed_args.secret,
115141
unrestricted=unrestricted,
142+
access_rules=access_rules,
116143
)
117144

118145
application_credential._info.pop('links', None)

openstackclient/tests/unit/identity/v3/fakes.py

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,14 @@
470470
app_cred_expires = datetime.datetime(2022, 1, 1, 0, 0)
471471
app_cred_expires_str = app_cred_expires.strftime('%Y-%m-%dT%H:%M:%S%z')
472472
app_cred_secret = 'moresecuresecret'
473+
app_cred_access_rules = (
474+
'[{"path": "/v2.1/servers", "method": "GET", "service": "compute"}]'
475+
)
476+
app_cred_access_rules_path = '/tmp/access_rules.json'
477+
access_rule_id = 'access-rule-id'
478+
access_rule_service = 'compute'
479+
access_rule_path = '/v2.1/servers'
480+
access_rule_method = 'GET'
473481
APP_CRED_BASIC = {
474482
'id': app_cred_id,
475483
'name': app_cred_name,
@@ -478,7 +486,8 @@
478486
'description': None,
479487
'expires_at': None,
480488
'unrestricted': False,
481-
'secret': app_cred_secret
489+
'secret': app_cred_secret,
490+
'access_rules': None
482491
}
483492
APP_CRED_OPTIONS = {
484493
'id': app_cred_id,
@@ -488,7 +497,25 @@
488497
'description': app_cred_description,
489498
'expires_at': app_cred_expires_str,
490499
'unrestricted': False,
491-
'secret': app_cred_secret
500+
'secret': app_cred_secret,
501+
'access_rules': None,
502+
}
503+
ACCESS_RULE = {
504+
'id': access_rule_id,
505+
'service': access_rule_service,
506+
'path': access_rule_path,
507+
'method': access_rule_method,
508+
}
509+
APP_CRED_ACCESS_RULES = {
510+
'id': app_cred_id,
511+
'name': app_cred_name,
512+
'project_id': project_id,
513+
'roles': app_cred_role,
514+
'description': None,
515+
'expires_at': None,
516+
'unrestricted': False,
517+
'secret': app_cred_secret,
518+
'access_rules': app_cred_access_rules
492519
}
493520

494521
registered_limit_id = 'registered-limit-id'
@@ -625,6 +652,8 @@ def __init__(self, **kwargs):
625652
self.application_credentials = mock.Mock()
626653
self.application_credentials.resource_class = fakes.FakeResource(None,
627654
{})
655+
self.access_rules = mock.Mock()
656+
self.access_rules.resource_class = fakes.FakeResource(None, {})
628657
self.inference_rules = mock.Mock()
629658
self.inference_rules.resource_class = fakes.FakeResource(None, {})
630659
self.registered_limits = mock.Mock()

0 commit comments

Comments
 (0)