Add basic CRUD for federation mapping

Change-Id: Ie3991efda6d2437821f67e3c87e111886578e830
Partially-Implements: blueprint keystone-federation-mapping
This commit is contained in:
lin-hua-cheng 2015-11-22 23:22:53 -08:00 committed by Lin Hua Cheng
parent 78bfa9727d
commit fbf10e9dad
16 changed files with 557 additions and 1 deletions

View File

@ -795,3 +795,31 @@ def identity_provider_delete(request, idp_id):
def identity_provider_list(request):
manager = keystoneclient(request, admin=True).federation.identity_providers
return manager.list()
def mapping_create(request, mapping_id, rules):
manager = keystoneclient(request, admin=True).federation.mappings
try:
return manager.create(mapping_id=mapping_id, rules=rules)
except keystone_exceptions.Conflict:
raise exceptions.Conflict()
def mapping_get(request, mapping_id):
manager = keystoneclient(request, admin=True).federation.mappings
return manager.get(mapping_id)
def mapping_update(request, mapping_id, rules):
manager = keystoneclient(request, admin=True).federation.mappings
return manager.update(mapping_id, rules=rules)
def mapping_delete(request, mapping_id):
manager = keystoneclient(request, admin=True).federation.mappings
return manager.delete(mapping_id)
def mapping_list(request):
manager = keystoneclient(request, admin=True).federation.mappings
return manager.list()

View File

@ -0,0 +1,86 @@
# 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 json
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
class CreateMappingForm(forms.SelfHandlingForm):
id = forms.CharField(label=_("Mapping ID"),
max_length=64,
help_text=_("User-defined unique id to identify "
"the mapping."))
rules = forms.CharField(label=_("Rules"),
widget=forms.widgets.Textarea(attrs={'rows': 4}),
help_text=_("Set of rules to map federation "
"protocol attributes to Identity "
"API objects."))
def handle(self, request, data):
try:
rules = json.loads(data["rules"])
new_mapping = api.keystone.mapping_create(
request,
data["id"],
rules=rules)
messages.success(request,
_("Mapping created successfully."))
return new_mapping
except exceptions.Conflict:
msg = _('Mapping ID "%s" is already used.') % data["id"]
messages.error(request, msg)
except (TypeError, ValueError):
msg = _("Unable to create mapping. Rules has malformed JSON data.")
messages.error(request, msg)
except Exception:
exceptions.handle(request,
_("Unable to create mapping."))
return False
class UpdateMappingForm(forms.SelfHandlingForm):
id = forms.CharField(label=_("Mapping ID"),
widget=forms.TextInput(
attrs={'readonly': 'readonly'}),
help_text=_("User-defined unique id to "
"identify the mapping."))
rules = forms.CharField(label=_("Rules"),
widget=forms.widgets.Textarea(attrs={'rows': 4}),
help_text=_("Set of rules to map federation "
"protocol attributes to Identity "
"API objects."))
def handle(self, request, data):
try:
rules = json.loads(data["rules"])
api.keystone.mapping_update(
request,
data['id'],
rules=rules)
messages.success(request,
_("Mapping updated successfully."))
return True
except (TypeError, ValueError):
msg = _("Unable to update mapping. Rules has malformed JSON data.")
messages.error(request, msg)
except Exception:
exceptions.handle(request,
_('Unable to update mapping.'))

View File

@ -0,0 +1,30 @@
# 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 _
import horizon
from openstack_dashboard.api import keystone
class Mappings(horizon.Panel):
name = _("Mappings")
slug = 'mappings'
policy_rules = (("identity", "identity:list_mappings"),)
@staticmethod
def can_register():
return (keystone.VERSIONS.active >= 3 and
keystone.is_federation_management_enabled())

View File

@ -0,0 +1,91 @@
# 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 json
from django.utils import safestring
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ungettext_lazy
from horizon import tables
from openstack_dashboard import api
class CreateMappingLink(tables.LinkAction):
name = "create"
verbose_name = _("Create Mapping")
url = "horizon:identity:mappings:create"
classes = ("ajax-modal",)
icon = "plus"
policy_rules = (("identity", "identity:create_mapping"),)
class EditMappingLink(tables.LinkAction):
name = "edit"
verbose_name = _("Edit")
url = "horizon:identity:mappings:update"
classes = ("ajax-modal",)
icon = "pencil"
policy_rules = (("identity", "identity:update_mapping"),)
class DeleteMappingsAction(tables.DeleteAction):
@staticmethod
def action_present(count):
return ungettext_lazy(
u"Delete Mapping",
u"Delete Mappings",
count
)
@staticmethod
def action_past(count):
return ungettext_lazy(
u"Deleted Mapping",
u"Deleted Mappings",
count
)
policy_rules = (("identity", "identity:delete_mapping"),)
def delete(self, request, obj_id):
api.keystone.mapping_delete(request, obj_id)
class MappingFilterAction(tables.FilterAction):
def filter(self, table, mappings, filter_string):
"""Naive case-insensitive search."""
q = filter_string.lower()
return [mapping for mapping in mappings
if q in mapping.ud.lower()]
def get_rules_as_json(mapping):
rules = getattr(mapping, 'rules', None)
if rules:
rules = json.dumps(rules, indent=4)
return safestring.mark_safe(rules)
class MappingsTable(tables.DataTable):
id = tables.Column('id', verbose_name=_('Mapping ID'))
description = tables.Column(get_rules_as_json,
verbose_name=_('Rules'))
class Meta(object):
name = "idp_mappings"
verbose_name = _("Attribute Mappings")
row_actions = (EditMappingLink, DeleteMappingsAction)
table_actions = (MappingFilterAction, CreateMappingLink,
DeleteMappingsAction)

View File

@ -0,0 +1,9 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% block modal-body-right %}
<h3>{% trans "Description:" %}</h3>
<p>{% trans "Create a mapping." %}</p>
<p>{% trans "A mapping is a set of rules to map federation protocol attributes to Identity API objects. An identity provider can have a single mapping specified per protocol. A mapping is simply a list of rules. The only Identity API objects that will support mapping are: user and group." %}</p>
<p>{% trans "A rule contains a remote attribute description and the destination local attribute." %}</p>
{% endblock %}

View File

@ -0,0 +1,8 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% block modal-body-right %}
<h3>{% trans "Description:" %}</h3>
<p>{% trans "Edit the mapping's details." %}</p>
<p>{% trans "A rule contains a remote attribute description and the destination local attribute." %}</p>
{% endblock %}

View File

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

View File

@ -0,0 +1,7 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Mappings" %}{% endblock %}
{% block main %}
{{ table.render }}
{% endblock %}

View File

@ -0,0 +1,7 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Update Mapping" %}{% endblock %}
{% block main %}
{% include 'identity/mappings/_update.html' %}
{% endblock %}

View File

@ -0,0 +1,107 @@
# 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 json
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
MAPPINGS_INDEX_URL = reverse('horizon:identity:mappings:index')
MAPPINGS_CREATE_URL = reverse('horizon:identity:mappings:create')
MAPPINGS_UPDATE_URL = reverse('horizon:identity:mappings:update',
args=['mapping_1'])
class MappingsViewTests(test.BaseAdminViewTests):
@test.create_stubs({api.keystone: ('mapping_list',)})
def test_index(self):
api.keystone.mapping_list(IgnoreArg()). \
AndReturn(self.idp_mappings.list())
self.mox.ReplayAll()
res = self.client.get(MAPPINGS_INDEX_URL)
self.assertTemplateUsed(res, 'identity/mappings/index.html')
self.assertItemsEqual(res.context['table'].data,
self.idp_mappings.list())
@test.create_stubs({api.keystone: ('mapping_create', )})
def test_create(self):
mapping = self.idp_mappings.first()
api.keystone.mapping_create(IgnoreArg(),
mapping.id,
rules=mapping.rules). \
AndReturn(mapping)
self.mox.ReplayAll()
formData = {'method': 'CreateMappingForm',
'id': mapping.id,
'rules': json.dumps(mapping.rules)}
res = self.client.post(MAPPINGS_CREATE_URL, formData)
self.assertNoFormErrors(res)
self.assertMessageCount(success=1)
@test.create_stubs({api.keystone: ('mapping_get',
'mapping_update')})
def test_update(self):
mapping = self.idp_mappings.first()
new_rules = [{"local": [], "remote": []}]
api.keystone.mapping_get(IsA(http.HttpRequest),
mapping.id). \
AndReturn(mapping)
api.keystone.mapping_update(IsA(http.HttpRequest),
mapping.id,
rules=new_rules). \
AndReturn(None)
self.mox.ReplayAll()
formData = {'method': 'UpdateMappingForm',
'id': mapping.id,
'rules': json.dumps(new_rules)}
res = self.client.post(MAPPINGS_UPDATE_URL, formData)
self.assertNoFormErrors(res)
self.assertMessageCount(success=1)
@test.create_stubs({api.keystone: ('mapping_list',
'mapping_delete')})
def test_delete(self):
mapping = self.idp_mappings.first()
api.keystone.mapping_list(IsA(http.HttpRequest)) \
.AndReturn(self.idp_mappings.list())
api.keystone.mapping_delete(IsA(http.HttpRequest),
mapping.id) \
.AndReturn(None)
self.mox.ReplayAll()
formData = {'action': 'idp_mappings__delete__%s' % mapping.id}
res = self.client.post(MAPPINGS_INDEX_URL, formData)
self.assertNoFormErrors(res)

View File

@ -0,0 +1,25 @@
# 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.mappings import views
urlpatterns = patterns(
'openstack_dashboard.dashboards.identity.mappings.views',
url(r'^$', views.IndexView.as_view(), name='index'),
url(r'^(?P<mapping_id>[^/]+)/update/$',
views.UpdateView.as_view(), name='update'),
url(r'^create/$', views.CreateView.as_view(), name='create'))

View File

@ -0,0 +1,101 @@
# 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 json
from django.core.urlresolvers import reverse
from django.core.urlresolvers import reverse_lazy
from django.utils.translation import ugettext_lazy as _
from horizon import exceptions
from horizon import forms
from horizon import messages
from horizon import tables
from horizon.utils import memoized
from openstack_dashboard import api
from openstack_dashboard import policy
from openstack_dashboard.dashboards.identity.mappings \
import forms as mapping_forms
from openstack_dashboard.dashboards.identity.mappings \
import tables as mapping_tables
class IndexView(tables.DataTableView):
table_class = mapping_tables.MappingsTable
template_name = 'identity/mappings/index.html'
page_title = _("Mappings")
def get_data(self):
mappings = []
if policy.check((("identity", "identity:list_mappings"),),
self.request):
try:
mappings = api.keystone.mapping_list(self.request)
except Exception:
exceptions.handle(
self.request,
_('Unable to retrieve mapping list.'))
else:
msg = _("Insufficient privilege level to view mapping "
"information.")
messages.info(self.request, msg)
return mappings
class UpdateView(forms.ModalFormView):
template_name = 'identity/mappings/update.html'
modal_header = _("Update Mapping")
form_id = "update_mapping_form"
form_class = mapping_forms.UpdateMappingForm
submit_label = _("Update Mapping")
submit_url = "horizon:identity:mappings:update"
success_url = reverse_lazy('horizon:identity:mappings:index')
page_title = _("Update Mapping")
@memoized.memoized_method
def get_object(self):
try:
return api.keystone.mapping_get(
self.request,
self.kwargs['mapping_id'])
except Exception:
redirect = reverse("horizon:identity:mappings:index")
exceptions.handle(self.request,
_('Unable to update mapping.'),
redirect=redirect)
def get_context_data(self, **kwargs):
context = super(UpdateView, self).get_context_data(**kwargs)
args = (self.get_object().id,)
context['submit_url'] = reverse(self.submit_url, args=args)
return context
def get_initial(self):
mapping = self.get_object()
rules = json.dumps(mapping.rules, indent=4)
return {'id': mapping.id,
'rules': rules}
class CreateView(forms.ModalFormView):
template_name = 'identity/mappings/create.html'
modal_header = _("Create Mapping")
form_id = "create_mapping_form"
form_class = mapping_forms.CreateMappingForm
submit_label = _("Create Mapping")
submit_url = reverse_lazy("horizon:identity:mappings:create")
success_url = reverse_lazy('horizon:identity:mappings:index')
page_title = _("Create Mapping")

View File

@ -0,0 +1,10 @@
# The slug of the panel to be added to HORIZON_CONFIG. Required.
PANEL = 'mappings'
# 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 = 'federation'
# Python panel class of the PANEL to be added.
ADD_PANEL = ('openstack_dashboard.dashboards.identity.'
'mappings.panel.Mappings')

View File

@ -24,6 +24,7 @@ from keystoneclient.v2_0 import roles
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 import domains
from keystoneclient.v3 import groups
from keystoneclient.v3 import role_assignments
@ -146,6 +147,7 @@ def data(TEST):
TEST.ec2 = utils.TestDataContainer()
TEST.identity_providers = utils.TestDataContainer()
TEST.idp_mappings = utils.TestDataContainer()
admin_role_dict = {'id': '1',
'name': 'admin'}
@ -387,3 +389,39 @@ def data(TEST):
identity_providers.IdentityProviderManager,
idp_dict_2)
TEST.identity_providers.add(idp_1, idp_2)
idp_mapping_dict = {
"id": "mapping_1",
"rules": [
{
"local": [
{
"user": {
"name": "{0}"
}
},
{
"group": {
"id": "0cd5e9"
}
}
],
"remote": [
{
"type": "UserName"
},
{
"type": "orgPersonType",
"not_any_of": [
"Contractor",
"Guest"
]
}
]
}
]
}
idp_mapping = mappings.Mapping(
mappings.MappingManager,
idp_mapping_dict)
TEST.idp_mappings.add(idp_mapping)

View File

@ -4,4 +4,6 @@ features:
[`blueprint keystone-federation-idp <https://blueprints.launchpad.net/horizon/+spec/keystone-federation-idp>`_]
Add support for managing keystone identity provider. To enable the panel,
set ``OPENSTACK_KEYSTONE_FEDERATION_MANAGEMENT`` in the local_settting.py to True.
- >
[`blueprint keystone-federation-mapping <https://blueprints.launchpad.net/horizon/+spec/keystone-federation-mapping>`_]
Add basic support for managing keystone federation mapping.