Add support for keystone access rules

Keystone implemented the ability to apply fine-grained access control
restrictions to application credentials[1]. This patch adds new fields
to the application credential creation form and detail view so that
horizon users can use this feature.

[1] http://specs.openstack.org/openstack/keystone-specs/specs/keystone/train/capabilities-app-creds.html

Depends-on: https://review.opendev.org/677585

Change-Id: I2d71392eb8569ffb8cb15af29eea76e120a158cc
This commit is contained in:
Colleen Murphy 2019-10-08 16:37:55 -07:00 committed by Akihiro Motoki
parent 307e884eaa
commit 4d1786c687
8 changed files with 112 additions and 5 deletions

View File

@ -96,7 +96,7 @@ pyScss==1.3.4
python-cinderclient==4.0.1 python-cinderclient==4.0.1
python-dateutil==2.5.3 python-dateutil==2.5.3
python-glanceclient==2.8.0 python-glanceclient==2.8.0
python-keystoneclient==3.15.0 python-keystoneclient==3.22.0
python-memcached==1.59 python-memcached==1.59
python-mimeparse==1.6.0 python-mimeparse==1.6.0
python-neutronclient==6.7.0 python-neutronclient==6.7.0

View File

@ -1003,12 +1003,14 @@ def application_credential_delete(request, application_credential_id):
@profiler.trace @profiler.trace
def application_credential_create(request, name, secret=None, def application_credential_create(request, name, secret=None,
description=None, expires_at=None, description=None, expires_at=None,
roles=None, unrestricted=False): roles=None, unrestricted=False,
access_rules=None):
user = request.user.id user = request.user.id
manager = keystoneclient(request).application_credentials manager = keystoneclient(request).application_credentials
try: try:
return manager.create(name=name, user=user, secret=secret, return manager.create(name=name, user=user, secret=secret,
description=description, expires_at=expires_at, description=description, expires_at=expires_at,
roles=roles, unrestricted=unrestricted) roles=roles, unrestricted=unrestricted,
access_rules=access_rules)
except keystone_exceptions.Conflict: except keystone_exceptions.Conflict:
raise exceptions.Conflict() raise exceptions.Conflict()

View File

@ -19,6 +19,7 @@ from django.conf import settings
from django.forms import widgets from django.forms import widgets
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.views.decorators.debug import sensitive_variables from django.views.decorators.debug import sensitive_variables
import yaml
from horizon import exceptions from horizon import exceptions
from horizon import forms from horizon import forms
@ -49,6 +50,10 @@ class CreateApplicationCredentialForm(forms.SelfHandlingForm):
widget=forms.widgets.SelectMultiple(), widget=forms.widgets.SelectMultiple(),
label=_("Roles"), label=_("Roles"),
required=False) required=False)
access_rules = forms.CharField(
widget=forms.Textarea(attrs={'rows': 5}),
label=_("Access Rules"),
required=False)
unrestricted = forms.BooleanField(label=_("Unrestricted (dangerous)"), unrestricted = forms.BooleanField(label=_("Unrestricted (dangerous)"),
required=False) required=False)
kubernetes_namespace = forms.CharField(max_length=255, kubernetes_namespace = forms.CharField(max_length=255,
@ -64,6 +69,9 @@ class CreateApplicationCredentialForm(forms.SelfHandlingForm):
role_names = [role['name'] for role in role_list] role_names = [role['name'] for role in role_list]
role_choices = ((name, name) for name in role_names) role_choices = ((name, name) for name in role_names)
self.fields['roles'].choices = role_choices self.fields['roles'].choices = role_choices
keystone_version = api.keystone.get_identity_api_version(request)
if keystone_version < (3, 13):
del self.fields['access_rules']
if not settings.KUBECONFIG_ENABLED: if not settings.KUBECONFIG_ENABLED:
self.fields['kubernetes_namespace'].widget = widgets.HiddenInput() self.fields['kubernetes_namespace'].widget = widgets.HiddenInput()
@ -95,6 +103,10 @@ class CreateApplicationCredentialForm(forms.SelfHandlingForm):
roles = [{'name': role_name} for role_name in data['roles']] roles = [{'name': role_name} for role_name in data['roles']]
else: else:
roles = None roles = None
if data.get('access_rules'):
access_rules = data['access_rules']
else:
access_rules = None
new_app_cred = api.keystone.application_credential_create( new_app_cred = api.keystone.application_credential_create(
request, request,
name=data['name'], name=data['name'],
@ -102,6 +114,7 @@ class CreateApplicationCredentialForm(forms.SelfHandlingForm):
secret=data['secret'] or None, secret=data['secret'] or None,
expires_at=expiration or None, expires_at=expiration or None,
roles=roles, roles=roles,
access_rules=access_rules,
unrestricted=data['unrestricted'] unrestricted=data['unrestricted']
) )
self.request.session['application_credential'] = \ self.request.session['application_credential'] = \
@ -118,6 +131,16 @@ class CreateApplicationCredentialForm(forms.SelfHandlingForm):
exceptions.handle( exceptions.handle(
request, _('Unable to create application credential: %s') % ex) request, _('Unable to create application credential: %s') % ex)
def clean(self):
cleaned_data = super(CreateApplicationCredentialForm, self).clean()
try:
cleaned_data['access_rules'] = yaml.safe_load(
cleaned_data['access_rules'])
except yaml.YAMLError:
msg = (_('Access rules must be a valid JSON or YAML list.'))
raise forms.ValidationError(msg)
return cleaned_data
class CreateSuccessfulForm(forms.SelfHandlingForm): class CreateSuccessfulForm(forms.SelfHandlingForm):
app_cred_id = forms.CharField( app_cred_id = forms.CharField(

View File

@ -12,6 +12,7 @@
</p> </p>
<p> <p>
{% blocktrans trimmed %} {% blocktrans trimmed %}
<b>Secret</b>:
You may provide your own secret, or one will be generated for you. Once your You may provide your own secret, or one will be generated for you. Once your
application credential is created, the secret will be revealed once. If you application credential is created, the secret will be revealed once. If you
lose the secret, you will have to generate a new application credential. lose the secret, you will have to generate a new application credential.
@ -19,6 +20,7 @@
</p> </p>
<p> <p>
{% blocktrans trimmed %} {% blocktrans trimmed %}
<b>Expiration Date/Time</b>:
You may give the application credential an expiration. The expiration will You may give the application credential an expiration. The expiration will
be in UTC. If you provide an expiration date with no expiration time, the be in UTC. If you provide an expiration date with no expiration time, the
time will be assumed to be 00:00:00. If you provide an expiration time with time will be assumed to be 00:00:00. If you provide an expiration time with
@ -27,6 +29,7 @@
</p> </p>
<p> <p>
{% blocktrans trimmed %} {% blocktrans trimmed %}
<b>Roles</b>:
You may select one or more roles for this application credential. If you do You may select one or more roles for this application credential. If you do
not select any, all of the roles you have assigned on the current project not select any, all of the roles you have assigned on the current project
will be applied to the application credential. will be applied to the application credential.
@ -34,6 +37,38 @@
</p> </p>
<p> <p>
{% blocktrans trimmed %} {% blocktrans trimmed %}
<b>Access Rules</b>:
If you want more fine-grained access control delegation, you can create one
or more access rules for this application credential. The list of access
rules must be a JSON- or YAML-formatted list of rules each containing a service type,
an HTTP method, and a URL path, for example:
<br />
<code>
[
<br />
&nbsp;&nbsp;{"service": "compute",
<br />
&nbsp;&nbsp;"method": "POST",
<br />
&nbsp;&nbsp;"path": "/v2.1/servers"}
<br />
]
<br />
</code>
or:
<br />
<code>
- service: compute
<br />
&nbsp;&nbsp;method: POST
<br />
&nbsp;&nbsp;path: /v2.1/servers
</code>
{% endblocktrans %}
</p>
<p>
{% blocktrans trimmed %}
<b>Unrestricted</b>:
By default, for security reasons, application credentials are forbidden from By default, for security reasons, application credentials are forbidden from
being used for creating additional application credentials or keystone being used for creating additional application credentials or keystone
trusts. If your application credential needs to be able to perform these trusts. If your application credential needs to be able to perform these
@ -43,6 +78,7 @@
<p> <p>
{% if kubeconfig_enabled %} {% if kubeconfig_enabled %}
{% blocktrans trimmed %} {% blocktrans trimmed %}
<b>Kubernetes Namespace</b>:
You can optionally provide a Kubernetes Namespace. It will be included in the You can optionally provide a Kubernetes Namespace. It will be included in the
kubeconfig file which can be downloaded from the next screen. kubeconfig file which can be downloaded from the next screen.
{% endblocktrans %} {% endblocktrans %}

View File

@ -26,10 +26,36 @@
<td class="word-wrap">{{ role.name }}</td> <td class="word-wrap">{{ role.name }}</td>
<td>{{ role.id }}</td> <td>{{ role.id }}</td>
<td>{{ role.domain_id | default:_("-") }}</td> <td>{{ role.domain_id | default:_("-") }}</td>
</tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</dd> </dd>
<dt>{% trans "Access Rules" %}</dt>
{% if application_credential.access_rules %}
<dd>
<table class="table table-striped table-hover">
<thead>
<tr>
<th><strong>{% trans "Service" %}</strong></th>
<th><strong>{% trans "Method" %}</strong></th>
<th><strong>{% trans "Path" %}</strong></th>
</tr>
</thead>
<tbody>
{% for rule in application_credential.access_rules %}
<tr>
<td>{{ rule.service }}</td>
<td>{{ rule.method }}</td>
<td>{{ rule.path }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</dd>
{% else %}
<dd>{{ _("-")}}</dd>
{% endif %}
<dt>{% trans "Expires" %}</dt> <dt>{% trans "Expires" %}</dt>
<dd>{{ application_credential.expires_at | default:_("-") }}</dd> <dd>{{ application_credential.expires_at | default:_("-") }}</dd>
<dt>{% trans "Unrestricted" %}</dt> <dt>{% trans "Unrestricted" %}</dt>

View File

@ -25,7 +25,9 @@ APP_CREDS_INDEX_URL = reverse('horizon:identity:application_credentials:index')
class ApplicationCredentialViewTests(test.TestCase): class ApplicationCredentialViewTests(test.TestCase):
def test_application_credential_create_get(self): @mock.patch.object(api.keystone, 'get_identity_api_version')
def test_application_credential_create_get(self, mock_identity_version):
mock_identity_version.return_value = (3, 13)
url = reverse('horizon:identity:application_credentials:create') url = reverse('horizon:identity:application_credentials:create')
res = self.client.get(url) res = self.client.get(url)
@ -33,11 +35,14 @@ class ApplicationCredentialViewTests(test.TestCase):
'identity/application_credentials/create.html') 'identity/application_credentials/create.html')
@mock.patch.object(api.keystone, 'application_credential_create') @mock.patch.object(api.keystone, 'application_credential_create')
@mock.patch.object(api.keystone, 'get_identity_api_version')
@mock.patch.object(api.keystone, 'application_credential_list') @mock.patch.object(api.keystone, 'application_credential_list')
def test_application_credential_create(self, mock_app_cred_list, def test_application_credential_create(self, mock_app_cred_list,
mock_identity_version,
mock_app_cred_create): mock_app_cred_create):
new_app_cred = self.application_credentials.first() new_app_cred = self.application_credentials.first()
mock_app_cred_create.return_value = new_app_cred mock_app_cred_create.return_value = new_app_cred
mock_identity_version.return_value = (3, 13)
data = { data = {
'name': new_app_cred.name, 'name': new_app_cred.name,
'description': new_app_cred.description 'description': new_app_cred.description
@ -47,6 +52,7 @@ class ApplicationCredentialViewTests(test.TestCase):
'description': new_app_cred.description, 'description': new_app_cred.description,
'expires_at': new_app_cred.expires_at, 'expires_at': new_app_cred.expires_at,
'roles': None, 'roles': None,
'access_rules': None,
'unrestricted': False, 'unrestricted': False,
'secret': None 'secret': None
} }
@ -91,12 +97,15 @@ class ApplicationCredentialViewTests(test.TestCase):
six.text_type(app_cred.id)) six.text_type(app_cred.id))
@mock.patch.object(api.keystone, 'application_credential_create') @mock.patch.object(api.keystone, 'application_credential_create')
@mock.patch.object(api.keystone, 'get_identity_api_version')
@mock.patch.object(api.keystone, 'application_credential_list') @mock.patch.object(api.keystone, 'application_credential_list')
def test_application_credential_openrc(self, mock_app_cred_list, def test_application_credential_openrc(self, mock_app_cred_list,
mock_identity_version,
mock_app_cred_create): mock_app_cred_create):
new_app_cred = self.application_credentials.first() new_app_cred = self.application_credentials.first()
mock_app_cred_create.return_value = new_app_cred mock_app_cred_create.return_value = new_app_cred
mock_identity_version.return_value = (3, 13)
data = { data = {
'name': new_app_cred.name, 'name': new_app_cred.name,
'description': new_app_cred.description 'description': new_app_cred.description
@ -114,12 +123,15 @@ class ApplicationCredentialViewTests(test.TestCase):
res, 'identity/application_credentials/openrc.sh.template') res, 'identity/application_credentials/openrc.sh.template')
@mock.patch.object(api.keystone, 'application_credential_create') @mock.patch.object(api.keystone, 'application_credential_create')
@mock.patch.object(api.keystone, 'get_identity_api_version')
@mock.patch.object(api.keystone, 'application_credential_list') @mock.patch.object(api.keystone, 'application_credential_list')
def test_application_credential_cloudsyaml(self, mock_app_cred_list, def test_application_credential_cloudsyaml(self, mock_app_cred_list,
mock_identity_version,
mock_app_cred_create): mock_app_cred_create):
new_app_cred = self.application_credentials.first() new_app_cred = self.application_credentials.first()
mock_app_cred_create.return_value = new_app_cred mock_app_cred_create.return_value = new_app_cred
mock_identity_version.return_value = (3, 13)
data = { data = {
'name': new_app_cred.name, 'name': new_app_cred.name,
'description': new_app_cred.description 'description': new_app_cred.description

View File

@ -0,0 +1,8 @@
---
features:
- |
Adds support for access rules for application credentials. Fine-grained
restrictions can now be applied to application credentials by supplying a
list of access rules upon creation. See the `keystone documentation
<https://docs.openstack.org/api-ref/identity/v3/#application-credentials>`_
for more information.

View File

@ -33,7 +33,7 @@ pymongo!=3.1,>=3.0.2 # Apache-2.0
pyScss!=1.3.5,>=1.3.4 # MIT License pyScss!=1.3.5,>=1.3.4 # MIT License
python-cinderclient>=4.0.1 # Apache-2.0 python-cinderclient>=4.0.1 # Apache-2.0
python-glanceclient>=2.8.0 # Apache-2.0 python-glanceclient>=2.8.0 # Apache-2.0
python-keystoneclient>=3.15.0 # Apache-2.0 python-keystoneclient>=3.22.0 # Apache-2.0
python-neutronclient>=6.7.0 # Apache-2.0 python-neutronclient>=6.7.0 # Apache-2.0
python-novaclient>=9.1.0 # Apache-2.0 python-novaclient>=9.1.0 # Apache-2.0
python-swiftclient>=3.2.0 # Apache-2.0 python-swiftclient>=3.2.0 # Apache-2.0