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:
parent
656490fee2
commit
2d69444bad
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
@ -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)
|
@ -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,)
|
@ -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 %}
|
@ -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>
|
||||
|
@ -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 %}
|
@ -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 %}
|
@ -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 %}
|
@ -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 %}
|
||||
|
@ -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 %}
|
@ -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 }}
|
@ -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 %}
|
@ -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')
|
@ -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'),
|
||||
]
|
@ -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
|
@ -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')
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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.
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user