Add support for identity provider protocol CRUD

Add the protocol tab under the Identity Provider detail
panel, this allows the user to manage the protocol on
the context of the Identity Provider,

Change-Id: I0e232b174382b1bc325e04cc343ae4d50e0cfed1
Implements: blueprint keystone-federation-protocol-mapping
This commit is contained in:
lin-hua-cheng 2015-11-23 10:22:15 -08:00
parent 8157b38f97
commit 2ce5de16ee
16 changed files with 535 additions and 3 deletions

View File

@ -824,3 +824,31 @@ def mapping_delete(request, mapping_id):
def mapping_list(request):
manager = keystoneclient(request, admin=True).federation.mappings
return manager.list()
def protocol_create(request, protocol_id, identity_provider, mapping):
manager = keystoneclient(request).federation.protocols
try:
return manager.create(protocol_id, identity_provider, mapping)
except keystone_exceptions.Conflict:
raise exceptions.Conflict()
def protocol_get(request, identity_provider, protocol):
manager = keystoneclient(request).federation.protocols
return manager.get(identity_provider, protocol)
def protocol_update(request, identity_provider, protocol, mapping):
manager = keystoneclient(request).federation.protocols
return manager.update(identity_provider, protocol, mapping)
def protocol_delete(request, identity_provider, protocol):
manager = keystoneclient(request).federation.protocols
return manager.delete(identity_provider, protocol)
def protocol_list(request, identity_provider):
manager = keystoneclient(request).federation.protocols
return manager.list(identity_provider)

View File

@ -0,0 +1,74 @@
# Copyright (C) 2015 Yahoo! Inc. All Rights Reserved.
#
# 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 logging
from django.utils.translation import ugettext_lazy as _
from horizon import exceptions
from horizon import forms
from horizon import messages
from openstack_dashboard import api
LOG = logging.getLogger(__name__)
class AddProtocolForm(forms.SelfHandlingForm):
idp_id = forms.CharField(label=_("Identity Provider ID"),
widget=forms.TextInput(
attrs={'readonly': 'readonly'}))
id = forms.CharField(label=_("Protocol ID"))
mapping_id = forms.ChoiceField(label=_("Mapping ID"))
def __init__(self, request, *args, **kwargs):
super(AddProtocolForm, self).__init__(request, *args, **kwargs)
self.populate_mapping_id_choices(request)
def populate_mapping_id_choices(self, request):
try:
mappings = api.keystone.mapping_list(request)
except Exception as e:
msg = _('Failed to get mapping list %s') % e
LOG.info(msg)
messages.error(request, msg)
choices = [(m.id, m.id) for m in mappings]
choices.sort()
if choices:
choices.insert(0, ("", _("Select Mapping")))
else:
choices.insert(0, ("", _("No mappings available")))
self.fields['mapping_id'].choices = choices
def handle(self, request, data):
try:
new_mapping = api.keystone.protocol_create(
request,
data["id"],
data["idp_id"],
data["mapping_id"])
messages.success(
request,
_("Identity provider protocol created successfully."))
return new_mapping
except exceptions.Conflict:
msg = _('Protocol ID "%s" is already used.') % data["id"]
messages.error(request, msg)
except Exception:
exceptions.handle(
request,
_("Unable to create identity provider protocol."))

View File

@ -0,0 +1,79 @@
# Copyright (C) 2015 Yahoo! Inc. All Rights Reserved.
#
# 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 logging
from django.core.urlresolvers import reverse
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
LOG = logging.getLogger(__name__)
class AddProtocol(policy.PolicyTargetMixin, tables.LinkAction):
name = "create"
verbose_name = _("Add Protocol")
url = "horizon:identity:identity_providers:protocols:create"
classes = ("ajax-modal",)
icon = "plus"
policy_rules = (("identity", "identity:create_protocol"),)
def get_link_url(self, datum=None):
idp_id = self.table.kwargs['identity_provider_id']
return reverse(self.url, args=(idp_id,))
class RemoveProtocol(policy.PolicyTargetMixin, tables.DeleteAction):
@staticmethod
def action_present(count):
return ungettext_lazy(
u"Delete Protocol",
u"Delete Protocols",
count
)
@staticmethod
def action_past(count):
return ungettext_lazy(
u"Deleted Protocol",
u"Deleted Protocols",
count
)
policy_rules = (("identity", "identity:delete_protocol"),)
def delete(self, request, obj_id):
identity_provider = self.table.kwargs['identity_provider_id']
protocol = obj_id
api.keystone.protocol_delete(request, identity_provider, protocol)
class ProtocolsTable(tables.DataTable):
protocol = tables.Column("id",
verbose_name=_("Protocol ID"))
mapping = tables.Column("mapping_id",
verbose_name=_("Mapping ID"))
def get_object_display(self, datum):
return datum.id
class Meta(object):
name = "idp_protocols"
verbose_name = _("Protocols")
table_actions = (AddProtocol, RemoveProtocol)
row_actions = (RemoveProtocol, )

View File

@ -0,0 +1,79 @@
# Copyright (C) 2015 Yahoo! Inc. All Rights Reserved.
#
# 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.core.urlresolvers import reverse
from django import http
from mox3.mox import IgnoreArg # noqa
from mox3.mox import IsA # noqa
from openstack_dashboard import api
from openstack_dashboard.test import helpers as test
IDPS_DETAIL_URL = reverse('horizon:identity:identity_providers:detail',
args=['idp_1'])
PROTOCOLS_CREATE_URL = reverse(
'horizon:identity:identity_providers:protocols:create',
args=['idp_1'])
class ProtocolsViewTests(test.BaseAdminViewTests):
@test.create_stubs({api.keystone: ('mapping_list',
'protocol_create', )})
def test_create(self):
idp = self.identity_providers.first()
protocol = self.idp_protocols.first()
api.keystone.mapping_list(IgnoreArg()). \
AndReturn(self.idp_mappings.list())
api.keystone.protocol_create(IgnoreArg(),
protocol.id,
idp.id,
protocol.mapping_id). \
AndReturn(protocol)
self.mox.ReplayAll()
formData = {'method': 'AddProtocolForm',
'id': protocol.id,
'idp_id': idp.id,
'mapping_id': protocol.mapping_id}
res = self.client.post(PROTOCOLS_CREATE_URL, formData)
self.assertNoFormErrors(res)
self.assertMessageCount(success=1)
@test.create_stubs({api.keystone: ('identity_provider_get',
'protocol_list',
'protocol_delete')})
def test_delete(self):
idp = self.identity_providers.first()
protocol = self.idp_protocols.first()
api.keystone.identity_provider_get(IsA(http.HttpRequest), idp.id). \
AndReturn(idp)
api.keystone.protocol_list(IsA(http.HttpRequest), idp.id). \
AndReturn(self.idp_protocols.list())
api.keystone.protocol_delete(IsA(http.HttpRequest),
idp.id,
protocol.id).AndReturn(None)
self.mox.ReplayAll()
formData = {'action': 'idp_protocols__delete__%s' % protocol.id}
res = self.client.post(IDPS_DETAIL_URL, formData)
self.assertNoFormErrors(res)

View File

@ -0,0 +1,27 @@
# Copyright (C) 2015 Yahoo! Inc. All Rights Reserved.
#
# 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 patterns
from django.conf.urls import url
from openstack_dashboard.dashboards.identity.identity_providers.protocols \
import views
PORTS = r'^(?P<protocol_id>[^/]+)/%s$'
urlpatterns = patterns(
'horizon.dashboards.identity.identity_providers.protocols.views',
url(r'^create/$', views.AddProtocolView.as_view(), name='create'),
)

View File

@ -0,0 +1,48 @@
# Copyright (C) 2015 Yahoo! Inc. All Rights Reserved.
#
# 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.core.urlresolvers import reverse
from django.utils.translation import ugettext_lazy as _
from horizon import forms
from openstack_dashboard.dashboards.identity.identity_providers.protocols \
import forms as protocol_forms
class AddProtocolView(forms.ModalFormView):
template_name = 'identity/identity_providers/protocols/create.html'
modal_header = _("Create Protocol")
form_id = "create_protocol_form"
form_class = protocol_forms.AddProtocolForm
submit_label = _("Create Protocol")
success_url = "horizon:identity:identity_providers:protocols_tab"
page_title = _("Create Protocol")
def __init__(self):
super(AddProtocolView, self).__init__()
def get_success_url(self):
return reverse(self.success_url,
args=(self.kwargs['identity_provider_id'],))
def get_context_data(self, **kwargs):
context = super(AddProtocolView, self).get_context_data(**kwargs)
context["submit_url"] = reverse(
"horizon:identity:identity_providers:protocols:create",
args=(self.kwargs['identity_provider_id'],))
return context
def get_initial(self):
return {"idp_id": self.kwargs['identity_provider_id']}

View File

@ -39,6 +39,14 @@ class EditIdPLink(tables.LinkAction):
policy_rules = (("identity", "identity:update_identity_provider"),)
class ManageProtocolsLink(tables.LinkAction):
name = "manage_protocols"
verbose_name = _("Manage Protocols")
url = "horizon:identity:identity_providers:protocols_tab"
icon = "pencil"
policy_rules = (("identity", "identity:list_protocols"),)
class DeleteIdPsAction(tables.DeleteAction):
@staticmethod
def action_present(count):
@ -70,7 +78,9 @@ class IdPFilterAction(tables.FilterAction):
class IdentityProvidersTable(tables.DataTable):
id = tables.Column('id', verbose_name=_('Identity Provider ID'))
id = tables.Column('id',
verbose_name=_('Identity Provider ID'),
link="horizon:identity:identity_providers:detail")
description = tables.Column(lambda obj: getattr(obj, 'description', None),
verbose_name=_('Description'))
enabled = tables.Column('enabled', verbose_name=_('Enabled'), status=True,
@ -87,5 +97,5 @@ class IdentityProvidersTable(tables.DataTable):
class Meta(object):
name = "identity_providers"
verbose_name = _("Identity Providers")
row_actions = (EditIdPLink, DeleteIdPsAction)
row_actions = (ManageProtocolsLink, EditIdPLink, DeleteIdPsAction)
table_actions = (IdPFilterAction, RegisterIdPLink, DeleteIdPsAction)

View File

@ -0,0 +1,48 @@
# Copyright (C) 2015 Yahoo! Inc. All Rights Reserved.
#
# 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 horizon import tabs
from openstack_dashboard.dashboards.identity.identity_providers.protocols \
import tables as ptbl
class OverviewTab(tabs.Tab):
name = _("Overview")
slug = "overview"
template_name = "identity/identity_providers/_detail_overview.html"
def get_context_data(self, request):
return {
"identity_provider": self.tab_group.kwargs['identity_provider']
}
class ProtocolsTab(tabs.TableTab):
table_classes = (ptbl.ProtocolsTable,)
name = _("Protocols")
slug = "protocols"
template_name = "horizon/common/_detail_table.html"
def get_idp_protocols_data(self):
return self.tab_group.kwargs['protocols']
class IdPDetailTabs(tabs.TabGroup):
slug = "idp_details"
tabs = (OverviewTab, ProtocolsTab)
sticky = True

View File

@ -0,0 +1,14 @@
{% load i18n sizeformat parse_date %}
<div class="detail">
<dl class="dl-horizontal">
<dt>{% trans "Identity Provider ID" %}</dt>
<dd>{{ identity_provider.id }}</dd>
<dt>{% trans "Remote IDs" %}</dt>
<dd>{{ identity_provider.remote_ids|join:", "|default:_("None") }}</dd>
<dt>{% trans "Description" %}</dt>
<dd>{{ identity_provider.description|default:_("None") }}</dd>
<dt>{% trans "Enabled" %}</dt>
<dd>{{ identity_provider.enabled|yesno|capfirst }}</dd>
</dl>
</div>

View File

@ -0,0 +1,8 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% block modal-body-right %}
<h3>{% trans "Description:" %}</h3>
<p>{% trans "Create a identity provider protocol." %}</p>
<p>{% blocktrans %} A protocol entry contains information that dictates which mapping rules to use for a given incoming request. An Identity Provider may have multiple supported protocols.{% endblocktrans %}</p>
{% endblock %}

View File

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

View File

@ -26,6 +26,8 @@ IDPS_INDEX_URL = reverse('horizon:identity:identity_providers:index')
IDPS_REGISTER_URL = reverse('horizon:identity:identity_providers:register')
IDPS_UPDATE_URL = reverse('horizon:identity:identity_providers:update',
args=['idp_1'])
IDPS_DETAIL_URL = reverse('horizon:identity:identity_providers:detail',
args=['idp_1'])
class IdPsViewTests(test.BaseAdminViewTests):
@ -109,3 +111,41 @@ class IdPsViewTests(test.BaseAdminViewTests):
res = self.client.post(IDPS_INDEX_URL, formData)
self.assertNoFormErrors(res)
@test.create_stubs({api.keystone: ('identity_provider_get',
'protocol_list')})
def test_detail(self):
idp = self.identity_providers.first()
api.keystone.identity_provider_get(IsA(http.HttpRequest), idp.id). \
AndReturn(idp)
api.keystone.protocol_list(IsA(http.HttpRequest), idp.id). \
AndReturn(self.idp_protocols.list())
self.mox.ReplayAll()
res = self.client.get(IDPS_DETAIL_URL)
self.assertTemplateUsed(
res, 'identity/identity_providers/_detail_overview.html')
self.assertTemplateUsed(res, 'horizon/common/_detail_table.html')
@test.create_stubs({api.keystone: ('identity_provider_get',
'protocol_list')})
def test_detail_protocols(self):
idp = self.identity_providers.first()
api.keystone.identity_provider_get(IsA(http.HttpRequest), idp.id). \
AndReturn(idp)
api.keystone.protocol_list(IsA(http.HttpRequest), idp.id). \
AndReturn(self.idp_protocols.list())
self.mox.ReplayAll()
res = self.client.get(IDPS_DETAIL_URL + '?tab=idp_details__protocols')
self.assertTemplateUsed(
res, 'identity/identity_providers/_detail_overview.html')
self.assertTemplateUsed(res, 'horizon/common/_detail_table.html')
self.assertItemsEqual(res.context['idp_protocols_table'].data,
self.idp_protocols.list())

View File

@ -12,15 +12,28 @@
# License for the specific language governing permissions and limitations
# under the License.
from django.conf.urls import include
from django.conf.urls import patterns
from django.conf.urls import url
from openstack_dashboard.dashboards.identity.identity_providers.protocols \
import urls as protocol_urls
from openstack_dashboard.dashboards.identity.identity_providers \
import views
urlpatterns = patterns(
'openstack_dashboard.dashboards.identity.identity_providers.views',
url(r'^$', views.IndexView.as_view(), name='index'),
url(r'^(?P<identity_provider_id>[^/]+)/detail/$',
views.DetailView.as_view(), name='detail'),
url(r'^(?P<identity_provider_id>[^/]+)/detail/'
'\?tab=idp_details__protocols$',
views.DetailView.as_view(),
name='protocols_tab'),
url(r'^(?P<identity_provider_id>[^/]+)/update/$',
views.UpdateView.as_view(), name='update'),
url(r'^register/$', views.RegisterView.as_view(), name='register'))
url(r'^register/$', views.RegisterView.as_view(), name='register'),
url(r'(?P<identity_provider_id>[^/]+)/protocols/',
include(protocol_urls, namespace='protocols')),
)

View File

@ -20,6 +20,7 @@ from horizon import exceptions
from horizon import forms
from horizon import messages
from horizon import tables
from horizon import tabs
from horizon.utils import memoized
from openstack_dashboard import api
@ -29,6 +30,8 @@ from openstack_dashboard.dashboards.identity.identity_providers \
import forms as idp_forms
from openstack_dashboard.dashboards.identity.identity_providers \
import tables as idp_tables
from openstack_dashboard.dashboards.identity.identity_providers \
import tabs as idp_tabs
class IndexView(tables.DataTableView):
@ -53,6 +56,51 @@ class IndexView(tables.DataTableView):
return idps
class DetailView(tabs.TabbedTableView):
tab_group_class = idp_tabs.IdPDetailTabs
template_name = 'horizon/common/_detail.html'
failure_url = reverse_lazy('horizon:identity:identity_providers:index')
page_title = "{{ identity_provider.id }}"
@memoized.memoized_method
def _get_data(self):
try:
return api.keystone.identity_provider_get(
self.request,
self.kwargs['identity_provider_id'])
except Exception:
redirect = reverse("horizon:identity:identity_providers:index")
exceptions.handle(self.request,
_('Unable to retrieve identity provider'
' information.'),
redirect=redirect)
@memoized.memoized_method
def _get_protocols_data(self):
try:
return api.keystone.protocol_list(
self.request,
self.kwargs['identity_provider_id'])
except Exception:
redirect = reverse("horizon:identity:identity_providers:index")
exceptions.handle(self.request,
_('Unable to retrieve protocol list.'),
redirect=redirect)
def get_context_data(self, **kwargs):
context = super(DetailView, self).get_context_data(**kwargs)
idp = self._get_data()
context["identity_provider"] = idp
return context
def get_tabs(self, request, *args, **kwargs):
identity_provider = self._get_data()
protocols = self._get_protocols_data()
return self.tab_group_class(request,
identity_provider=identity_provider,
protocols=protocols, **kwargs)
class UpdateView(forms.ModalFormView):
template_name = 'identity/identity_providers/update.html'
modal_header = _("Update Identity Provider")

View File

@ -25,6 +25,7 @@ from keystoneclient.v2_0 import tenants
from keystoneclient.v2_0 import users
from keystoneclient.v3.contrib.federation import identity_providers
from keystoneclient.v3.contrib.federation import mappings
from keystoneclient.v3.contrib.federation import protocols
from keystoneclient.v3 import domains
from keystoneclient.v3 import groups
from keystoneclient.v3 import role_assignments
@ -148,6 +149,7 @@ def data(TEST):
TEST.identity_providers = utils.TestDataContainer()
TEST.idp_mappings = utils.TestDataContainer()
TEST.idp_protocols = utils.TestDataContainer()
admin_role_dict = {'id': '1',
'name': 'admin'}
@ -425,3 +427,10 @@ def data(TEST):
mappings.MappingManager(None),
idp_mapping_dict)
TEST.idp_mappings.add(idp_mapping)
idp_protocol_dict_1 = {'id': 'protocol_1',
'mapping_id': 'mapping_1'}
idp_protocol = protocols.Protocol(
protocols.ProtocolManager,
idp_protocol_dict_1)
TEST.idp_protocols.add(idp_protocol)