Add support for application credentials

This patch adds support for creating application credentials in
keystone[1]. Application credentials can be created by any user for
themselves. An application credential is created for the currently
selected project. A user may provide their own secret for the application
credential, or may allow keystone to generate a secret for them. After
the application credential is created, the secret is revealed once to
the user. At that point they may download a clouds.yaml or openrc file
that contains the application credential secret and will enable them to
use it to authenticate. The secret is not revealed again.

[1] https://docs.openstack.org/keystone/latest/user/application_credentials.html

bp application-credentials

Depends-On: https://review.openstack.org/557927
Depends-On: https://review.openstack.org/557932
Change-Id: Ida2e836cf81d2b96e0b66afed29a900c312223a4
This commit is contained in:
Colleen Murphy 2018-03-11 22:38:28 +01:00 committed by Colleen Murphy
parent 656490fee2
commit 2d69444bad
23 changed files with 910 additions and 2 deletions

View File

@ -99,7 +99,7 @@ pyScss==1.3.4
python-cinderclient==3.3.0
python-dateutil==2.5.3
python-glanceclient==2.8.0
python-keystoneclient==3.8.0
python-keystoneclient==3.15.0
python-mimeparse==1.6.0
python-neutronclient==6.7.0
python-novaclient==9.1.0

View File

@ -1082,3 +1082,37 @@ def protocol_delete(request, identity_provider, protocol):
def protocol_list(request, identity_provider):
manager = keystoneclient(request).federation.protocols
return manager.list(identity_provider)
@profiler.trace
def application_credential_list(request, filters=None):
user = request.user.id
manager = keystoneclient(request).application_credentials
return manager.list(user=user, **filters)
@profiler.trace
def application_credential_get(request, application_credential_id):
user = request.user.id
manager = keystoneclient(request).application_credentials
return manager.get(application_credential=application_credential_id,
user=user)
@profiler.trace
def application_credential_delete(request, application_credential_id):
user = request.user.id
manager = keystoneclient(request).application_credentials
return manager.delete(application_credential=application_credential_id,
user=user)
@profiler.trace
def application_credential_create(request, name, secret=None,
description=None, expires_at=None,
roles=None, unrestricted=False):
user = request.user.id
manager = keystoneclient(request).application_credentials
return manager.create(name=name, user=user, secret=secret,
description=description, expires_at=expires_at,
roles=roles, unrestricted=unrestricted)

View File

@ -0,0 +1,125 @@
# Copyright 2018 SUSE Linux GmbH
#
# 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 datetime
import logging
from django.utils.translation import ugettext_lazy as _
from django.views.decorators.debug import sensitive_variables
from horizon import exceptions
from horizon import forms
from horizon import messages
from openstack_dashboard import api
LOG = logging.getLogger(__name__)
class CreateApplicationCredentialForm(forms.SelfHandlingForm):
# Hide the domain_id and domain_name by default
name = forms.CharField(max_length=255, label=_("Name"))
description = forms.CharField(
widget=forms.widgets.Textarea(attrs={'rows': 4}),
label=_("Description"),
required=False)
secret = forms.CharField(max_length=255, label=_("Secret"), required=False)
expiration_date = forms.DateField(
widget=forms.widgets.DateInput(attrs={'type': 'date'}),
label=_("Expiration Date"),
required=False)
expiration_time = forms.TimeField(
widget=forms.widgets.TimeInput(attrs={'type': 'time'}),
label=_("Expiration Time"),
required=False)
roles = forms.MultipleChoiceField(
widget=forms.widgets.SelectMultiple(),
label=_("Roles"),
required=False)
unrestricted = forms.BooleanField(label=_("Unrestricted (dangerous)"),
required=False)
def __init__(self, request, *args, **kwargs):
self.next_view = kwargs.pop('next_view', None)
super(CreateApplicationCredentialForm, self).__init__(request, *args,
**kwargs)
role_list = self.request.user.roles
role_names = [role['name'] for role in role_list]
role_choices = ((name, name) for name in role_names)
self.fields['roles'].choices = role_choices
# We have to protect the entire "data" dict because it contains the
# secret string.
@sensitive_variables('data')
def handle(self, request, data):
try:
LOG.info('Creating application credential with name "%s"',
data['name'])
expiration = None
if data['expiration_date']:
if data['expiration_time']:
expiration_time = data['expiration_time']
else:
expiration_time = datetime.datetime.min.time()
expiration = datetime.datetime.combine(
data['expiration_date'], expiration_time)
else:
if data['expiration_time']:
expiration_time = data['expiration_time']
expiration_date = datetime.date.today()
expiration = datetime.datetime.combine(expiration_date,
expiration_time)
if data['roles']:
# the role list received from the form is a list of dicts
# encoded as strings
roles = [{'name': role_name} for role_name in data['roles']]
else:
roles = None
new_app_cred = api.keystone.application_credential_create(
request,
name=data['name'],
description=data['description'] or None,
secret=data['secret'] or None,
expires_at=expiration or None,
roles=roles,
unrestricted=data['unrestricted'] or None
)
self.request.session['application_credential'] = \
new_app_cred.to_dict()
request.method = 'GET'
return self.next_view.as_view()(request)
except exceptions.Conflict:
msg = (_('Application credential name "%s" is already used.')
% data['name'])
messages.error(request, msg)
except Exception:
exceptions.handle(request,
_('Unable to create application credential.'))
class CreateSuccessfulForm(forms.SelfHandlingForm):
app_cred_id = forms.CharField(
label=_("ID"),
widget=forms.TextInput(attrs={'readonly': 'readonly'}))
app_cred_name = forms.CharField(
label=_("Name"),
widget=forms.TextInput(attrs={'readonly': 'readonly'}))
app_cred_secret = forms.CharField(
label=_("Secret"),
widget=forms.widgets.Textarea(
attrs={'rows': 3, 'readonly': 'readonly'}))
def handle(self, request, data):
pass

View File

@ -0,0 +1,34 @@
# Copyright 2018 SUSE Linux GmbH
#
# 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.
from django.utils.translation import ugettext_lazy as _
import horizon
from openstack_dashboard.api import keystone
class ApplicationCredentialsPanel(horizon.Panel):
name = _("Application Credentials")
slug = 'application_credentials'
policy_rules = (('identity', 'identity:list_application_credentials'),)
@staticmethod
def can_register():
return keystone.VERSIONS.active >= 3
def can_access(self, context):
request = context['request']
keystone_version = keystone.get_identity_api_version(request)
return keystone_version >= (3, 10)

View File

@ -0,0 +1,84 @@
# 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.
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ungettext_lazy
from horizon import tables
from openstack_dashboard import api
from openstack_dashboard import policy
APP_CRED_DETAILS_LINK = "horizon:identity:application_credentials:detail"
class CreateApplicationCredentialLink(tables.LinkAction):
name = "create"
verbose_name = _("Create Application Credential")
url = "horizon:identity:application_credentials:create"
classes = ("ajax-modal",)
icon = "plus"
policy_rules = (('identity', 'identity:create_application_credential'),)
class DeleteApplicationCredentialAction(policy.PolicyTargetMixin,
tables.DeleteAction):
@staticmethod
def action_present(count):
return ungettext_lazy(
"Delete Application Credential",
"Delete Application Credentials",
count
)
@staticmethod
def action_past(count):
return ungettext_lazy(
"Deleted Application Credential",
"Deleted Application Credentialss",
count
)
policy_rules = (("identity", "identity:delete_application_credential"),)
def delete(self, request, obj_id):
api.keystone.application_credential_delete(request, obj_id)
class ApplicationCredentialFilterAction(tables.FilterAction):
filter_type = "query"
filter_choices = (("name", _("Application Credential Name ="), True))
def _role_names(obj):
return [role['name'].encode('utf-8') for role in obj.roles]
class ApplicationCredentialsTable(tables.DataTable):
name = tables.WrappingColumn('name',
link=APP_CRED_DETAILS_LINK,
verbose_name=_('Name'))
project_id = tables.Column('project_id', verbose_name=_('Project ID'))
description = tables.Column('description',
verbose_name=_('Description'))
expires_at = tables.Column('expires_at',
verbose_name=_('Expiration'))
id = tables.Column('id', verbose_name=_('ID'))
roles = tables.Column(_role_names, verbose_name=_('Roles'))
class Meta(object):
name = "application_credentials"
verbose_name = _("Application Credentials")
row_actions = (DeleteApplicationCredentialAction,)
table_actions = (CreateApplicationCredentialLink,
DeleteApplicationCredentialAction,
ApplicationCredentialFilterAction,)

View File

@ -0,0 +1,43 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% block modal-body-right %}
<h3>{% trans "Description:" %}</h3>
<p>{% trans "Create a new application credential." %}</p>
<p>
{% blocktrans trimmed %}
The application credential will be created for the currently selected
project.
{% endblocktrans %}
</p>
<p>
{% blocktrans trimmed %}
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
lose the secret, you will have to generate a new application credential.
{% endblocktrans %}
</p>
<p>
{% blocktrans trimmed %}
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
time will be assumed to be 00:00:00. If you provide an expiration time with
no expiration date, the date will be assumed to be today.
{% endblocktrans %}
</p>
<p>
{% blocktrans trimmed %}
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
will be applied to the application credential.
{% endblocktrans %}
</p>
<p>
{% blocktrans trimmed %}
By default, for security reasons, application credentials are forbidden from
being used for creating additional application credentials or keystone
trusts. If your application credential needs to be able to perform these
actions, check "unrestricted".
{% endblocktrans %}
</p>
{% endblock %}

View File

@ -0,0 +1,39 @@
{% load i18n %}
<div class="detail">
<dl class="dl-horizontal">
<dt>{% trans "Name" %}</dt>
<dd data-display="{{ application_credential.name }}">{{ application_credential.name }}</dd>
<dt>{% trans "ID" %}</dt>
<dd>{{ application_credential.id }}</dd>
<dt>{% trans "Project ID" %}</dt>
<dd>{{ application_credential.project_id }}</dd>
<dt>{% trans "Description" %}</dt>
<dd>{{ application_credential.description | default:_("-") }}</dd>
<dt>{% trans "Roles" %}</dt>
<dd>
<table class="table table-striped table-hover">
<thead>
<tr>
<th><strong>{% trans "Name" %}</strong></th>
<th><strong>{% trans "ID" %}</strong></th>
<th><strong>{% trans "Domain" %}</strong></th>
</tr>
</thead>
<tbody>
{% for role in application_credential.roles %}
<tr>
<td>{{ role.name }}</td>
<td>{{ role.id }}</td>
<td>{{ role.domain_id | default:_("-") }}</td>
{% endfor %}
</tbody>
</table>
</dd>
<dt>{% trans "Expires" %}</dt>
<dd>{{ application_credential.expires_at | default:_("-") }}</dd>
<dt>{% trans "Unrestricted" %}</dt>
<dd>{{ application_credential.unrestricted | yesno }}</dd>
</dl>
</div>

View File

@ -0,0 +1,35 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% block header %}
<div class="modal-header">
<h3 class="modal-title">{% block modal-header %}{{ modal_header }}{% endblock %}</h3>
</div>
{% endblock %}
{% block modal-body-right %}
<h3>{% trans "Your application credential" %}</h3>
<p>
{% blocktrans trimmed %}
Please capture the application credential ID and secret in order to
provide them to your application.
{% endblocktrans %}
</p>
<p class="alert alert-warning">
{% blocktrans trimmed %}
The application credential secret will not be available after closing this
page, so you must capture it now or download it. If you lose this secret,
you must generate a new application credential.
{% endblocktrans %}
</p>
{% endblock %}
{% block modal-footer %}
<a href="{{ download_openrc_url }}" class="btn btn-default">
<span class="fa fa-download"></span>
{{ download_openrc_label }}
</a>
<a href="{{ download_clouds_yaml_url }}" class="btn btn-default">
<span class="fa fa-download"></span>
{{ download_clouds_yaml_label }}
</a>
<a onClick="location.href='{{cancel_url}}'" href="{{ cancel_url }}" class="btn btn-default">{{ cancel_label }}</a>
{% endblock %}

View File

@ -0,0 +1,34 @@
# This is a clouds.yaml file, which can be used by OpenStack tools as a source
# of configuration on how to connect to a cloud. If this is your only cloud,
# just put this file in ~/.config/openstack/clouds.yaml and tools like
# python-openstackclient will just work with no further config. (You will need
# to add your password to the auth section)
# If you have more than one cloud account, add the cloud entry to the clouds
# section of your existing file and you can refer to them by name with
# OS_CLOUD={{ cloud_name }} or --os-cloud={{ cloud_name }}
clouds:
{{ cloud_name }}:
{% if profile %}
profile: {{ profile }}
{% endif %}
auth:
{% if not profile %}
auth_url: {{ auth_url }}
{% endif %}
application_credential_id: "{{ application_credential_id }}"
application_credential_secret: "{{ application_credential_secret }}"
{% if not profile %}
{% if regions %}
regions:
{% for r in regions %}
- {{ r }}
{% endfor %}
{% else %}
{% if region %}
region_name: "{{ region }}"
{% endif %}
{% endif %}
interface: "{{ interface }}"
identity_api_version: 3
auth_type: "v3applicationcredential"
{% endif %}

View File

@ -0,0 +1,7 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Create Application Credential" %}{% endblock %}
{% block main %}
{% include 'identity/application_credentials/_create.html' %}
{% endblock %}

View File

@ -0,0 +1,17 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Application Credential Details" %}{% endblock %}
{% block page_header %}
{% include "horizon/common/_detail_header.html" %}
{% endblock %}
{% block main %}
<div class="row">
<div class="col-sm-12">
{% include "identity/application_credentials/_detail_overview.html" %}
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,11 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Application Credentials" %}{% endblock %}
{% block page_header %}
{% include "horizon/common/_domain_page_header.html" with title=page_title %}
{% endblock page_header %}
{% block main %}
{{ table.render }}
{% endblock %}

View File

@ -0,0 +1,9 @@
{% load shellfilter %}#!/usr/bin/env bash
export OS_AUTH_TYPE=v3applicationcredential
export OS_AUTH_URL={{ auth_url }}
export OS_IDENTITY_API_VERSION=3
export OS_REGION_NAME="{{ region|shellfilter }}"
export OS_INTERFACE={{ interface }}
export OS_APPLICATION_CREDENTIAL_ID={{ application_credential_id }}
export OS_APPLICATION_CREDENTIAL_SECRET={{ application_credential_secret }}

View File

@ -0,0 +1,7 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Application Credential Details" %}{% endblock %}
{% block main %}
{% include 'identity/application_credentials/_success.html' %}
{% endblock %}

View File

@ -0,0 +1,137 @@
# Copyright 2018 SUSE Linux GmbH
#
# 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 mock
import six
from django.urls import reverse
from openstack_dashboard import api
from openstack_dashboard.test import helpers as test
APP_CREDS_INDEX_URL = reverse('horizon:identity:application_credentials:index')
class ApplicationCredentialViewTests(test.TestCase):
def test_application_credential_create_get(self):
url = reverse('horizon:identity:application_credentials:create')
res = self.client.get(url)
self.assertTemplateUsed(res,
'identity/application_credentials/create.html')
@mock.patch.object(api.keystone, 'application_credential_create')
@mock.patch.object(api.keystone, 'application_credential_list')
def test_application_credential_create(self, mock_app_cred_list,
mock_app_cred_create):
new_app_cred = self.application_credentials.first()
mock_app_cred_create.return_value = new_app_cred
data = {
'name': new_app_cred.name,
'description': new_app_cred.description
}
api_data = {
'name': new_app_cred.name,
'description': new_app_cred.description,
'expires_at': new_app_cred.expires_at,
'roles': None,
'unrestricted': None,
'secret': None
}
url = reverse('horizon:identity:application_credentials:create')
res = self.client.post(url, data)
self.assertNoFormErrors(res)
self.assertEqual(res.status_code, 200)
mock_app_cred_create.assert_called_once_with(test.IsHttpRequest(),
**api_data)
@mock.patch.object(api.keystone, 'application_credential_get')
def test_application_credential_detail_get(self, mock_app_cred_get):
app_cred = self.application_credentials.list()[1]
mock_app_cred_get.return_value = app_cred
res = self.client.get(
reverse('horizon:identity:application_credentials:detail',
args=[app_cred.id]))
self.assertTemplateUsed(
res, 'identity/application_credentials/detail.html')
self.assertEqual(res.context['application_credential'].name,
app_cred.name)
mock_app_cred_get.assert_called_once_with(test.IsHttpRequest(),
six.text_type(app_cred.id))
@mock.patch.object(api.keystone, 'application_credential_get')
def test_application_credential_detail_get_with_exception(
self, mock_app_cred_get):
app_cred = self.application_credentials.list()[1]
mock_app_cred_get.side_effect = self.exceptions.keystone
url = reverse('horizon:identity:application_credentials:detail',
args=[app_cred.id])
res = self.client.get(url)
self.assertRedirectsNoFollow(res, APP_CREDS_INDEX_URL)
mock_app_cred_get.assert_called_once_with(test.IsHttpRequest(),
six.text_type(app_cred.id))
@mock.patch.object(api.keystone, 'application_credential_create')
@mock.patch.object(api.keystone, 'application_credential_list')
def test_application_credential_openrc(self, mock_app_cred_list,
mock_app_cred_create):
new_app_cred = self.application_credentials.first()
mock_app_cred_create.return_value = new_app_cred
data = {
'name': new_app_cred.name,
'description': new_app_cred.description
}
url = reverse('horizon:identity:application_credentials:create')
res = self.client.post(url, data)
download_url = (
'horizon:identity:application_credentials:download_openrc'
)
url = reverse(download_url)
res = self.client.get(url)
self.assertEqual(res.status_code, 200)
self.assertTemplateUsed(
res, 'identity/application_credentials/openrc.sh.template')
@mock.patch.object(api.keystone, 'application_credential_create')
@mock.patch.object(api.keystone, 'application_credential_list')
def test_application_credential_cloudsyaml(self, mock_app_cred_list,
mock_app_cred_create):
new_app_cred = self.application_credentials.first()
mock_app_cred_create.return_value = new_app_cred
data = {
'name': new_app_cred.name,
'description': new_app_cred.description
}
url = reverse('horizon:identity:application_credentials:create')
res = self.client.post(url, data)
download_url = (
'horizon:identity:application_credentials:download_clouds_yaml'
)
url = reverse(download_url)
res = self.client.get(url)
self.assertEqual(res.status_code, 200)
self.assertTemplateUsed(
res, 'identity/application_credentials/clouds.yaml.template')

View File

@ -0,0 +1,33 @@
# Copyright 2018 SUSE Linux GmbH
#
# 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.
from django.conf.urls import url
from openstack_dashboard.dashboards.identity.application_credentials \
import views
urlpatterns = [
url(r'^$', views.IndexView.as_view(), name='index'),
url(r'^create/$', views.CreateView.as_view(), name='create'),
url(r'^(?P<application_credential_id>[^/]+)/detail/$',
views.DetailView.as_view(), name='detail'),
url(r'^success/$',
views.CreateSuccessfulView.as_view(), name='success'),
url(r'^download_openrc/$',
views.download_rc_file, name='download_openrc'),
url(r'^download_clouds_yaml/$',
views.download_clouds_yaml_file, name='download_clouds_yaml'),
]

View File

@ -0,0 +1,195 @@
# Copyright 2018 SUSE Linux GmbH
#
# 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.
from django.conf import settings
from django import http
from django.template.loader import render_to_string
from django.urls import reverse
from django.urls import reverse_lazy
from django.utils.translation import ugettext_lazy as _
from openstack_auth import utils
from horizon import exceptions
from horizon import forms
from horizon import tables
from horizon.utils import memoized
from horizon import views
from openstack_dashboard import api
from openstack_dashboard.dashboards.identity.application_credentials \
import forms as project_forms
from openstack_dashboard.dashboards.identity.application_credentials \
import tables as project_tables
INDEX_URL = "horizon:identity:application_credentials:index"
class IndexView(tables.DataTableView):
table_class = project_tables.ApplicationCredentialsTable
template_name = 'identity/application_credentials/index.html'
page_title = _("Application Credentials")
def needs_filter_first(self, table):
return self._needs_filter_first
def get_data(self):
app_creds = []
filters = self.get_filters()
self._needs_filter_first = False
# If filter_first is set and if there are not other filters
# selected, then search criteria must be provided
# and return an empty list
filter_first = getattr(settings, 'FILTER_DATA_FIRST', {})
if (filter_first.get('identity.application_credentials', False) and
not filters):
self._needs_filter_first = True
return app_creds
try:
app_creds = api.keystone.application_credential_list(
self.request, filters=filters)
except Exception:
exceptions.handle(
self.request,
_('Unable to retrieve application credential list.'))
return app_creds
class CreateView(forms.ModalFormView):
template_name = 'identity/application_credentials/create.html'
form_id = 'create_application_credential_form'
form_class = project_forms.CreateApplicationCredentialForm
submit_label = _("Create Application Credential")
submit_url = reverse_lazy(
'horizon:identity:application_credentials:create')
success_url = reverse_lazy(
'horizon:identity:application_credentials:success')
page_title = _("Create Application Credential")
def get_form_kwargs(self):
kwargs = super(CreateView, self).get_form_kwargs()
kwargs['next_view'] = CreateSuccessfulView
return kwargs
class CreateSuccessfulView(forms.ModalFormView):
template_name = 'identity/application_credentials/success.html'
page_title = _("Your Application Credential")
form_class = project_forms.CreateSuccessfulForm
model_id = "create_application_credential_successful_modal"
success_url = reverse_lazy(
'horizon:identity:application_credentials:index')
cancel_label = _("Close")
download_openrc_label = _("Download openrc file")
download_clouds_yaml_label = _("Download clouds.yaml")
def get_context_data(self, **kwargs):
context = super(CreateSuccessfulView, self).get_context_data(**kwargs)
context['download_openrc_label'] = self.download_openrc_label
context['download_clouds_yaml_label'] = self.download_clouds_yaml_label
context['download_openrc_url'] = reverse(
'horizon:identity:application_credentials:download_openrc')
context['download_clouds_yaml_url'] = reverse(
'horizon:identity:application_credentials:download_clouds_yaml')
return context
def get_initial(self):
app_cred = self.request.session['application_credential']
return {
'app_cred_id': app_cred['id'],
'app_cred_name': app_cred['name'],
'app_cred_secret': app_cred['secret']
}
def _get_context(request):
auth_url = api.base.url_for(request,
'identity',
endpoint_type='publicURL')
auth_url, url_fixed = utils.fix_auth_url_version_prefix(auth_url)
interface = 'public'
region = getattr(request.user, 'services_region', '')
app_cred = request.session['application_credential']
context = dict(auth_url=auth_url,
interface=interface,
region=region,
application_credential_id=app_cred['id'],
application_credential_name=app_cred['name'],
application_credential_secret=app_cred['secret'])
return context
def _render_attachment(filename, template, context, request):
content = render_to_string(template, context, request=request)
disposition = 'attachment; filename="%s"' % filename
response = http.HttpResponse(content, content_type="text/plain")
response['Content-Disposition'] = disposition.encode('utf-8')
response['Content-Length'] = str(len(response.content))
return response
def download_rc_file(request):
context = _get_context(request)
template = 'identity/application_credentials/openrc.sh.template'
filename = 'app-cred-%s-openrc.sh' % context['application_credential_name']
response = _render_attachment(filename, template, context, request)
return response
def download_clouds_yaml_file(request):
context = _get_context(request)
context['cloud_name'] = getattr(
settings, "OPENSTACK_CLOUDS_YAML_NAME", 'openstack')
context['profile'] = getattr(
settings, "OPENSTACK_CLOUDS_YAML_PROFILE", None)
context['regions'] = [
region_tuple[1] for region_tuple in getattr(
settings, "AVAILABLE_REGIONS", [])
]
template = 'identity/application_credentials/clouds.yaml.template'
filename = 'clouds.yaml'
return _render_attachment(filename, template, context, request)
class DetailView(views.HorizonTemplateView):
template_name = 'identity/application_credentials/detail.html'
page_title = "{{ application_credential.name }}"
def get_context_data(self, **kwargs):
context = super(DetailView, self).get_context_data(**kwargs)
app_cred = self.get_data()
table = project_tables.ApplicationCredentialsTable(self.request)
context["application_credential"] = app_cred
context["url"] = reverse(INDEX_URL)
context["actions"] = table.render_row_actions(app_cred)
return context
@memoized.memoized_method
def get_data(self):
try:
app_cred_id = self.kwargs['application_credential_id']
app_cred = api.keystone.application_credential_get(self.request,
app_cred_id)
except Exception:
exceptions.handle(
self.request,
_('Unable to retrieve application credential details.'),
redirect=reverse(INDEX_URL))
return app_cred

View File

@ -0,0 +1,10 @@
# The slug of the panel to be added to HORIZON_CONFIG. Required.
PANEL = 'application_credentials'
# The slug of the dashboard the PANEL associated with. Required.
PANEL_DASHBOARD = 'identity'
# The slug of the panel group the PANEL is associated with.
PANEL_GROUP = 'default'
# Python panel class of the PANEL to be added.
ADD_PANEL = ('openstack_dashboard.dashboards.identity.application_credentials'
'.panel.ApplicationCredentialsPanel')

View File

@ -328,4 +328,10 @@ TEST_GLOBAL_MOCKS_ON_PANELS = {
'.network_qos.panel.NetworkQoS.can_access'),
'return_value': True,
},
'application_credentials': {
'method': ('openstack_dashboard.dashboards.identity'
'.application_credentials.panel'
'.ApplicationCredentialsPanel.can_access'),
'return_value': True,
},
}

View File

@ -23,6 +23,7 @@ from keystoneclient.v2_0 import ec2
from keystoneclient.v2_0 import roles
from keystoneclient.v2_0 import tenants
from keystoneclient.v2_0 import users
from keystoneclient.v3 import application_credentials
from keystoneclient.v3.contrib.federation import identity_providers
from keystoneclient.v3.contrib.federation import mappings
from keystoneclient.v3.contrib.federation import protocols
@ -135,6 +136,8 @@ def data(TEST):
TEST.idp_mappings = utils.TestDataContainer()
TEST.idp_protocols = utils.TestDataContainer()
TEST.application_credentials = utils.TestDataContainer()
admin_role_dict = {'id': '1',
'name': 'admin'}
admin_role = roles.Role(roles.RoleManager, admin_role_dict, loaded=True)
@ -430,3 +433,42 @@ def data(TEST):
idp_protocol_dict_1,
loaded=True)
TEST.idp_protocols.add(idp_protocol)
app_cred_dict = {
'id': 'ac1',
'name': 'created',
'secret': 'secret',
'project': 'p1',
'description': 'newly created application credential',
'expires_at': None,
'unrestricted': False,
'roles': [
{'id': 'r1',
'name': 'Member',
'domain': None},
{'id': 'r2',
'name': 'admin',
'domain': None}
]
}
app_cred_create = application_credentials.ApplicationCredential(
None, app_cred_dict)
app_cred_dict = {
'id': 'ac2',
'name': 'detail',
'project': 'p1',
'description': 'existing application credential',
'expires_at': None,
'unrestricted': False,
'roles': [
{'id': 'r1',
'name': 'Member',
'domain': None},
{'id': 'r2',
'name': 'admin',
'domain': None}
]
}
app_cred_detail = application_credentials.ApplicationCredential(
None, app_cred_dict)
TEST.application_credentials.add(app_cred_create, app_cred_detail)

View File

@ -0,0 +1,6 @@
---
features:
- |
[`blueprint application-credentials <https://blueprints.launchpad.net/horizon/+spec/application-credentials>`_]
Adds a new panel for creating, viewing, and deleting keystone application
credentials.

View File

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