summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDavid Lyle <david.lyle@hp.com>2013-05-15 14:45:03 -0600
committerDavid Lyle <david.lyle@hp.com>2013-05-24 09:07:54 -0600
commitd774f23ea2a426ee4a943c62d31a6c12fbb93bec (patch)
tree9c65f18080b315c8cb64e88e950cb322ffc4716e
parent24d9f140f5584a6360a1b073ffc1e3bfd69ca6d6 (diff)
downloadhorizon-d774f23ea2a426ee4a943c62d31a6c12fbb93bec.tar.gz
Adding CRUD for roles
This adds basic support for CRUD on roles for admin users. The CRUD operations in keystone are only exposed without an extension in V3. So if the python-keystoneclient does not support V3, the panel will not be registered. Support for blocking create, edit and delete in LDAP case added. Implements: blueprint admin-role-crud Change-Id: Ibd727df1392ab9a748ab585620c1ff3dc57d3efb
-rw-r--r--openstack_dashboard/api/keystone.py25
-rw-r--r--openstack_dashboard/dashboards/admin/dashboard.py2
-rw-r--r--openstack_dashboard/dashboards/admin/roles/__init__.py0
-rw-r--r--openstack_dashboard/dashboards/admin/roles/forms.py48
-rw-r--r--openstack_dashboard/dashboards/admin/roles/panel.py30
-rw-r--r--openstack_dashboard/dashboards/admin/roles/tables.py76
-rw-r--r--openstack_dashboard/dashboards/admin/roles/templates/roles/_create.html25
-rw-r--r--openstack_dashboard/dashboards/admin/roles/templates/roles/_update.html25
-rw-r--r--openstack_dashboard/dashboards/admin/roles/templates/roles/create.html12
-rw-r--r--openstack_dashboard/dashboards/admin/roles/templates/roles/index.html11
-rw-r--r--openstack_dashboard/dashboards/admin/roles/templates/roles/update.html12
-rw-r--r--openstack_dashboard/dashboards/admin/roles/tests.py114
-rw-r--r--openstack_dashboard/dashboards/admin/roles/urls.py24
-rw-r--r--openstack_dashboard/dashboards/admin/roles/views.py74
-rw-r--r--openstack_dashboard/local/local_settings.py.example3
-rw-r--r--openstack_dashboard/test/settings.py3
16 files changed, 481 insertions, 3 deletions
diff --git a/openstack_dashboard/api/keystone.py b/openstack_dashboard/api/keystone.py
index f0a7551c3..8b704e52d 100644
--- a/openstack_dashboard/api/keystone.py
+++ b/openstack_dashboard/api/keystone.py
@@ -360,6 +360,26 @@ def user_update_tenant(request, user, project, admin=True):
return manager.update(user, project=project)
+def role_create(request, name):
+ manager = keystoneclient(request, admin=True).roles
+ return manager.create(name)
+
+
+def role_get(request, role_id):
+ manager = keystoneclient(request, admin=True).roles
+ return manager.get(role_id)
+
+
+def role_update(request, role_id, name=None):
+ manager = keystoneclient(request, admin=True).roles
+ return manager.update(role_id, name)
+
+
+def role_delete(request, role_id):
+ manager = keystoneclient(request, admin=True).roles
+ return manager.delete(role_id)
+
+
def role_list(request):
""" Returns a global list of available roles. """
return keystoneclient(request, admin=True).roles.list()
@@ -452,6 +472,11 @@ def keystone_can_edit_project():
return backend_settings.get('can_edit_project', True)
+def keystone_can_edit_role():
+ backend_settings = getattr(settings, "OPENSTACK_KEYSTONE_BACKEND", {})
+ return backend_settings.get('can_edit_role', True)
+
+
def keystone_backend_name():
if hasattr(settings, "OPENSTACK_KEYSTONE_BACKEND"):
return settings.OPENSTACK_KEYSTONE_BACKEND['name']
diff --git a/openstack_dashboard/dashboards/admin/dashboard.py b/openstack_dashboard/dashboards/admin/dashboard.py
index 43597defa..e3b2b3de0 100644
--- a/openstack_dashboard/dashboards/admin/dashboard.py
+++ b/openstack_dashboard/dashboards/admin/dashboard.py
@@ -23,7 +23,7 @@ class SystemPanels(horizon.PanelGroup):
slug = "admin"
name = _("System Panel")
panels = ('overview', 'instances', 'volumes', 'flavors',
- 'images', 'domains', 'projects', 'users',
+ 'images', 'domains', 'projects', 'users', 'roles',
'networks', 'routers', 'info')
diff --git a/openstack_dashboard/dashboards/admin/roles/__init__.py b/openstack_dashboard/dashboards/admin/roles/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/openstack_dashboard/dashboards/admin/roles/__init__.py
diff --git a/openstack_dashboard/dashboards/admin/roles/forms.py b/openstack_dashboard/dashboards/admin/roles/forms.py
new file mode 100644
index 000000000..03ad1f06b
--- /dev/null
+++ b/openstack_dashboard/dashboards/admin/roles/forms.py
@@ -0,0 +1,48 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2013 Hewlett-Packard Development Company, L.P.
+#
+# 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 exceptions
+from horizon import forms
+from horizon import messages
+
+from openstack_dashboard import api
+
+
+class CreateRoleForm(forms.SelfHandlingForm):
+ name = forms.CharField(label=_("Role Name"))
+
+ def handle(self, request, data):
+ try:
+ new_user = api.keystone.role_create(request, data["name"])
+ messages.success(request, _("Role created successfully."))
+ return new_user
+ except:
+ exceptions.handle(request, _('Unable to create role.'))
+
+
+class UpdateRoleForm(forms.SelfHandlingForm):
+ id = forms.CharField(label=_("ID"), widget=forms.HiddenInput)
+ name = forms.CharField(label=_("Role Name"))
+
+ def handle(self, request, data):
+ try:
+ api.keystone.role_update(request, data['id'], data["name"])
+ messages.success(request, _("Role updated successfully."))
+ return True
+ except:
+ exceptions.handle(request, _('Unable to update role.'))
diff --git a/openstack_dashboard/dashboards/admin/roles/panel.py b/openstack_dashboard/dashboards/admin/roles/panel.py
new file mode 100644
index 000000000..f8fa67480
--- /dev/null
+++ b/openstack_dashboard/dashboards/admin/roles/panel.py
@@ -0,0 +1,30 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2013 Hewlett-Packard Development Company, L.P.
+#
+# 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.keystone import VERSIONS as IDENTITY_VERSIONS
+from openstack_dashboard.dashboards.admin import dashboard
+
+
+class Roles(horizon.Panel):
+ name = _("Roles")
+ slug = 'roles'
+
+if IDENTITY_VERSIONS.active >= 3:
+ dashboard.Admin.register(Roles)
diff --git a/openstack_dashboard/dashboards/admin/roles/tables.py b/openstack_dashboard/dashboards/admin/roles/tables.py
new file mode 100644
index 000000000..97431e184
--- /dev/null
+++ b/openstack_dashboard/dashboards/admin/roles/tables.py
@@ -0,0 +1,76 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2013 Hewlett-Packard Development Company, L.P.
+#
+# 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 tables
+
+from openstack_dashboard import api
+
+
+LOG = logging.getLogger(__name__)
+
+
+class CreateRoleLink(tables.LinkAction):
+ name = "create"
+ verbose_name = _("Create Role")
+ url = "horizon:admin:roles:create"
+ classes = ("ajax-modal", "btn-create")
+
+ def allowed(self, request, role):
+ return api.keystone.keystone_can_edit_role()
+
+
+class EditRoleLink(tables.LinkAction):
+ name = "edit"
+ verbose_name = _("Edit")
+ url = "horizon:admin:roles:update"
+ classes = ("ajax-modal", "btn-edit")
+
+ def allowed(self, request, role):
+ return api.keystone.keystone_can_edit_role()
+
+
+class DeleteRolesAction(tables.DeleteAction):
+ data_type_singular = _("Role")
+ data_type_plural = _("Roles")
+
+ def allowed(self, request, role):
+ return api.keystone.keystone_can_edit_role()
+
+ def delete(self, request, obj_id):
+ api.keystone.role_delete(request, obj_id)
+
+
+class RoleFilterAction(tables.FilterAction):
+ def filter(self, table, roles, filter_string):
+ """ Naive case-insensitive search """
+ q = filter_string.lower()
+ return [role for role in roles
+ if q in role.name.lower()]
+
+
+class RolesTable(tables.DataTable):
+ name = tables.Column('name', verbose_name=_('Role Name'))
+ id = tables.Column('id', verbose_name=_('Role ID'))
+
+ class Meta:
+ name = "roles"
+ verbose_name = _("Roles")
+ row_actions = (EditRoleLink, DeleteRolesAction)
+ table_actions = (RoleFilterAction, CreateRoleLink, DeleteRolesAction)
diff --git a/openstack_dashboard/dashboards/admin/roles/templates/roles/_create.html b/openstack_dashboard/dashboards/admin/roles/templates/roles/_create.html
new file mode 100644
index 000000000..4ddd8bc73
--- /dev/null
+++ b/openstack_dashboard/dashboards/admin/roles/templates/roles/_create.html
@@ -0,0 +1,25 @@
+{% extends "horizon/common/_modal_form.html" %}
+{% load i18n %}
+{% load url from future %}
+
+{% block form_id %}create_role_form{% endblock %}
+{% block form_action %}{% url 'horizon:admin:roles:create' %}{% endblock %}
+
+{% block modal-header %}{% trans "Create Role" %}{% endblock %}
+
+{% block modal-body %}
+<div class="left">
+ <fieldset>
+ {% include "horizon/common/_form_fields.html" %}
+ </fieldset>
+</div>
+<div class="right">
+ <h3>{% trans "Description" %}:</h3>
+ <p>{% trans "From here you can create a new role." %}</p>
+</div>
+{% endblock %}
+
+{% block modal-footer %}
+ <input class="btn btn-primary pull-right" type="submit" value="{% trans "Create Role" %}" />
+ <a href="{% url 'horizon:admin:roles:index' %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
+{% endblock %}
diff --git a/openstack_dashboard/dashboards/admin/roles/templates/roles/_update.html b/openstack_dashboard/dashboards/admin/roles/templates/roles/_update.html
new file mode 100644
index 000000000..6b3c4c42b
--- /dev/null
+++ b/openstack_dashboard/dashboards/admin/roles/templates/roles/_update.html
@@ -0,0 +1,25 @@
+{% extends "horizon/common/_modal_form.html" %}
+{% load i18n %}
+{% load url from future %}
+
+{% block form_id %}update_role_form{% endblock %}
+{% block form_action %}{% url 'horizon:admin:roles:update' role.id %}{% endblock %}
+
+{% block modal-header %}{% trans "Update Role" %}{% endblock %}
+
+{% block modal-body %}
+<div class="left">
+ <fieldset>
+ {% include "horizon/common/_form_fields.html" %}
+ </fieldset>
+</div>
+<div class="right">
+ <h3>{% trans "Description" %}:</h3>
+ <p>{% trans "From here you can edit the role's details." %}</p>
+</div>
+{% endblock %}
+
+{% block modal-footer %}
+ <input class="btn btn-primary pull-right" type="submit" value="{% trans "Update Role" %}" />
+ <a href="{% url 'horizon:admin:roles:index' %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
+{% endblock %}
diff --git a/openstack_dashboard/dashboards/admin/roles/templates/roles/create.html b/openstack_dashboard/dashboards/admin/roles/templates/roles/create.html
new file mode 100644
index 000000000..42835afea
--- /dev/null
+++ b/openstack_dashboard/dashboards/admin/roles/templates/roles/create.html
@@ -0,0 +1,12 @@
+{% extends 'base.html' %}
+{% load i18n %}
+{% block title %}{% trans "Create Role" %}{% endblock %}
+
+{% block page_header %}
+ {# to make searchable false, just remove it from the include statement #}
+ {% include "horizon/common/_page_header.html" with title=_("Create Role") %}
+{% endblock page_header %}
+
+{% block main %}
+ {% include 'admin/roles/_create.html' %}
+{% endblock %}
diff --git a/openstack_dashboard/dashboards/admin/roles/templates/roles/index.html b/openstack_dashboard/dashboards/admin/roles/templates/roles/index.html
new file mode 100644
index 000000000..6a61c9797
--- /dev/null
+++ b/openstack_dashboard/dashboards/admin/roles/templates/roles/index.html
@@ -0,0 +1,11 @@
+{% extends 'base.html' %}
+{% load i18n %}
+{% block title %}{% trans "Roles" %}{% endblock %}
+
+{% block page_header %}
+ {% include "horizon/common/_page_header.html" with title=_("Roles") %}
+{% endblock page_header %}
+
+{% block main %}
+ {{ table.render }}
+{% endblock %}
diff --git a/openstack_dashboard/dashboards/admin/roles/templates/roles/update.html b/openstack_dashboard/dashboards/admin/roles/templates/roles/update.html
new file mode 100644
index 000000000..233d54cd6
--- /dev/null
+++ b/openstack_dashboard/dashboards/admin/roles/templates/roles/update.html
@@ -0,0 +1,12 @@
+{% extends 'base.html' %}
+{% load i18n %}
+{% block title %}{% trans "Update Role" %}{% endblock %}
+
+{% block page_header %}
+ {# to make searchable false, just remove it from the include statement #}
+ {% include "horizon/common/_page_header.html" with title=_("Update Role") %}
+{% endblock page_header %}
+
+{% block main %}
+ {% include 'admin/roles/_update.html' %}
+{% endblock %}
diff --git a/openstack_dashboard/dashboards/admin/roles/tests.py b/openstack_dashboard/dashboards/admin/roles/tests.py
new file mode 100644
index 000000000..f4f5db84b
--- /dev/null
+++ b/openstack_dashboard/dashboards/admin/roles/tests.py
@@ -0,0 +1,114 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2013 Hewlett-Packard Development Company, L.P.
+#
+# 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 import http
+from django.core.urlresolvers import reverse
+
+from mox import IgnoreArg, IsA
+
+from openstack_dashboard import api
+from openstack_dashboard.test import helpers as test
+
+
+ROLES_INDEX_URL = reverse('horizon:admin:roles:index')
+ROLES_CREATE_URL = reverse('horizon:admin:roles:create')
+ROLES_UPDATE_URL = reverse('horizon:admin:roles:update', args=[1])
+
+
+class RolesViewTests(test.BaseAdminViewTests):
+ @test.create_stubs({api.keystone: ('role_list',)})
+ def test_index(self):
+ api.keystone.role_list(IgnoreArg()).AndReturn(self.roles.list())
+
+ self.mox.ReplayAll()
+
+ res = self.client.get(ROLES_INDEX_URL)
+ self.assertContains(res, 'Create Role')
+ self.assertContains(res, 'Edit')
+ self.assertContains(res, 'Delete Role')
+
+ self.assertTemplateUsed(res, 'admin/roles/index.html')
+ self.assertItemsEqual(res.context['table'].data, self.roles.list())
+
+ @test.create_stubs({api.keystone: ('role_list',
+ 'keystone_can_edit_role', )})
+ def test_index_with_keystone_can_edit_role_false(self):
+ role = self.roles.first()
+
+ api.keystone.role_list(IgnoreArg()).AndReturn(self.roles.list())
+ api.keystone.keystone_can_edit_role() \
+ .MultipleTimes().AndReturn(False)
+ self.mox.ReplayAll()
+
+ res = self.client.get(ROLES_INDEX_URL)
+
+ self.assertNotContains(res, 'Create Role')
+ self.assertNotContains(res, 'Edit')
+ self.assertNotContains(res, 'Delete Role')
+
+ self.assertTemplateUsed(res, 'admin/roles/index.html')
+ self.assertItemsEqual(res.context['table'].data, self.roles.list())
+
+ @test.create_stubs({api.keystone: ('role_create', )})
+ def test_create(self):
+ role = self.roles.first()
+
+ api.keystone.role_create(IgnoreArg(), role.name).AndReturn(role)
+
+ self.mox.ReplayAll()
+
+ formData = {'method': 'CreateRoleForm', 'name': role.name}
+ res = self.client.post(ROLES_CREATE_URL, formData)
+
+ self.assertNoFormErrors(res)
+ self.assertMessageCount(success=1)
+
+ @test.create_stubs({api.keystone: ('role_get', 'role_update')})
+ def test_update(self):
+ role = self.roles.first()
+ new_role_name = 'test_name'
+
+ api.keystone.role_get(IsA(http.HttpRequest), role.id).AndReturn(role)
+ api.keystone.role_update(IsA(http.HttpRequest),
+ role.id,
+ new_role_name).AndReturn(None)
+
+ self.mox.ReplayAll()
+
+ formData = {'method': 'UpdateRoleForm',
+ 'id': role.id,
+ 'name': new_role_name}
+
+ res = self.client.post(ROLES_UPDATE_URL, formData)
+
+ self.assertNoFormErrors(res)
+ self.assertMessageCount(success=1)
+
+ @test.create_stubs({api.keystone: ('role_list', 'role_delete')})
+ def test_delete(self):
+ role = self.roles.first()
+
+ api.keystone.role_list(IsA(http.HttpRequest)) \
+ .AndReturn(self.roles.list())
+ api.keystone.role_delete(IsA(http.HttpRequest),
+ role.id).AndReturn(None)
+
+ self.mox.ReplayAll()
+
+ formData = {'action': 'roles__delete__%s' % role.id}
+ res = self.client.post(ROLES_INDEX_URL, formData)
+
+ self.assertNoFormErrors(res)
diff --git a/openstack_dashboard/dashboards/admin/roles/urls.py b/openstack_dashboard/dashboards/admin/roles/urls.py
new file mode 100644
index 000000000..333cf2ea9
--- /dev/null
+++ b/openstack_dashboard/dashboards/admin/roles/urls.py
@@ -0,0 +1,24 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2013 Hewlett-Packard Development Company, L.P.
+#
+# 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.defaults import patterns, url
+
+from .views import IndexView, CreateView, UpdateView
+
+urlpatterns = patterns('openstack_dashboard.dashboards.admin.roles.views',
+ url(r'^$', IndexView.as_view(), name='index'),
+ url(r'^(?P<role_id>[^/]+)/update/$', UpdateView.as_view(), name='update'),
+ url(r'^create/$', CreateView.as_view(), name='create'))
diff --git a/openstack_dashboard/dashboards/admin/roles/views.py b/openstack_dashboard/dashboards/admin/roles/views.py
new file mode 100644
index 000000000..42e81b21b
--- /dev/null
+++ b/openstack_dashboard/dashboards/admin/roles/views.py
@@ -0,0 +1,74 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2013 Hewlett-Packard Development Company, L.P.
+#
+# 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, reverse_lazy
+from django.utils.translation import ugettext_lazy as _
+
+from horizon import exceptions
+from horizon import forms
+from horizon import tables
+
+from openstack_dashboard import api
+from .forms import CreateRoleForm, UpdateRoleForm
+from .tables import RolesTable
+
+
+class IndexView(tables.DataTableView):
+ table_class = RolesTable
+ template_name = 'admin/roles/index.html'
+
+ def get_data(self):
+ roles = []
+ try:
+ roles = api.keystone.role_list(self.request)
+ except:
+ exceptions.handle(self.request,
+ _('Unable to retrieve roles list.'))
+ return roles
+
+
+class UpdateView(forms.ModalFormView):
+ form_class = UpdateRoleForm
+ template_name = 'admin/roles/update.html'
+ success_url = reverse_lazy('horizon:admin:roles:index')
+
+ def get_object(self):
+ if not hasattr(self, "_object"):
+ try:
+ self._object = api.keystone.role_get(self.request,
+ self.kwargs['role_id'])
+ except:
+ redirect = reverse("horizon:admin:roles:index")
+ exceptions.handle(self.request,
+ _('Unable to update role.'),
+ redirect=redirect)
+ return self._object
+
+ def get_context_data(self, **kwargs):
+ context = super(UpdateView, self).get_context_data(**kwargs)
+ context['role'] = self.get_object()
+ return context
+
+ def get_initial(self):
+ role = self.get_object()
+ return {'id': role.id,
+ 'name': role.name}
+
+
+class CreateView(forms.ModalFormView):
+ form_class = CreateRoleForm
+ template_name = 'admin/roles/create.html'
+ success_url = reverse_lazy('horizon:admin:roles:index')
diff --git a/openstack_dashboard/local/local_settings.py.example b/openstack_dashboard/local/local_settings.py.example
index 4c3897a2c..990b3fd7e 100644
--- a/openstack_dashboard/local/local_settings.py.example
+++ b/openstack_dashboard/local/local_settings.py.example
@@ -137,7 +137,8 @@ OPENSTACK_KEYSTONE_BACKEND = {
'name': 'native',
'can_edit_user': True,
'can_edit_project': True,
- 'can_edit_domain': True
+ 'can_edit_domain': True,
+ 'can_edit_role': True
}
OPENSTACK_HYPERVISOR_FEATURES = {
diff --git a/openstack_dashboard/test/settings.py b/openstack_dashboard/test/settings.py
index 1e02e5ce6..78afb1f34 100644
--- a/openstack_dashboard/test/settings.py
+++ b/openstack_dashboard/test/settings.py
@@ -77,7 +77,8 @@ OPENSTACK_KEYSTONE_BACKEND = {
'name': 'native',
'can_edit_user': True,
'can_edit_project': True,
- 'can_edit_domain': True
+ 'can_edit_domain': True,
+ 'can_edit_role': True
}
OPENSTACK_QUANTUM_NETWORK = {