Merge "Separating Identity Dashboard and using RBAC"

changes/12/114312/5
Jenkins 8 years ago committed by Gerrit Code Review
commit 6393dbe003
  1. 49
      horizon/base.py
  2. 12
      horizon/templates/horizon/_subnav_list.html
  3. 27
      horizon/templatetags/horizon.py
  4. 77
      horizon/test/tests/base.py
  5. 5
      openstack_dashboard/api/keystone.py
  6. 8
      openstack_dashboard/dashboards/admin/dashboard.py
  7. 23
      openstack_dashboard/dashboards/admin/groups/constants.py
  8. 0
      openstack_dashboard/dashboards/identity/__init__.py
  9. 28
      openstack_dashboard/dashboards/identity/dashboard.py
  10. 0
      openstack_dashboard/dashboards/identity/domains/__init__.py
  11. 8
      openstack_dashboard/dashboards/identity/domains/constants.py
  12. 6
      openstack_dashboard/dashboards/identity/domains/panel.py
  13. 4
      openstack_dashboard/dashboards/identity/domains/tables.py
  14. 0
      openstack_dashboard/dashboards/identity/domains/templates/domains/index.html
  15. 6
      openstack_dashboard/dashboards/identity/domains/tests.py
  16. 2
      openstack_dashboard/dashboards/identity/domains/urls.py
  17. 38
      openstack_dashboard/dashboards/identity/domains/views.py
  18. 2
      openstack_dashboard/dashboards/identity/domains/workflows.py
  19. 0
      openstack_dashboard/dashboards/identity/groups/__init__.py
  20. 23
      openstack_dashboard/dashboards/identity/groups/constants.py
  21. 0
      openstack_dashboard/dashboards/identity/groups/forms.py
  22. 5
      openstack_dashboard/dashboards/identity/groups/panel.py
  23. 2
      openstack_dashboard/dashboards/identity/groups/tables.py
  24. 2
      openstack_dashboard/dashboards/identity/groups/templates/groups/_add_non_member.html
  25. 4
      openstack_dashboard/dashboards/identity/groups/templates/groups/_create.html
  26. 4
      openstack_dashboard/dashboards/identity/groups/templates/groups/_update.html
  27. 2
      openstack_dashboard/dashboards/identity/groups/templates/groups/add_non_member.html
  28. 2
      openstack_dashboard/dashboards/identity/groups/templates/groups/create.html
  29. 0
      openstack_dashboard/dashboards/identity/groups/templates/groups/index.html
  30. 0
      openstack_dashboard/dashboards/identity/groups/templates/groups/manage.html
  31. 2
      openstack_dashboard/dashboards/identity/groups/templates/groups/update.html
  32. 2
      openstack_dashboard/dashboards/identity/groups/tests.py
  33. 2
      openstack_dashboard/dashboards/identity/groups/urls.py
  34. 25
      openstack_dashboard/dashboards/identity/groups/views.py
  35. 0
      openstack_dashboard/dashboards/identity/projects/__init__.py
  36. 6
      openstack_dashboard/dashboards/identity/projects/panel.py
  37. 24
      openstack_dashboard/dashboards/identity/projects/tables.py
  38. 0
      openstack_dashboard/dashboards/identity/projects/templates/projects/index.html
  39. 0
      openstack_dashboard/dashboards/identity/projects/templates/projects/usage.html
  40. 83
      openstack_dashboard/dashboards/identity/projects/tests.py
  41. 2
      openstack_dashboard/dashboards/identity/projects/urls.py
  42. 50
      openstack_dashboard/dashboards/identity/projects/views.py
  43. 8
      openstack_dashboard/dashboards/identity/projects/workflows.py
  44. 0
      openstack_dashboard/dashboards/identity/roles/__init__.py
  45. 0
      openstack_dashboard/dashboards/identity/roles/forms.py
  46. 6
      openstack_dashboard/dashboards/identity/roles/panel.py
  47. 4
      openstack_dashboard/dashboards/identity/roles/tables.py
  48. 4
      openstack_dashboard/dashboards/identity/roles/templates/roles/_create.html
  49. 4
      openstack_dashboard/dashboards/identity/roles/templates/roles/_update.html
  50. 2
      openstack_dashboard/dashboards/identity/roles/templates/roles/create.html
  51. 0
      openstack_dashboard/dashboards/identity/roles/templates/roles/index.html
  52. 2
      openstack_dashboard/dashboards/identity/roles/templates/roles/update.html
  53. 10
      openstack_dashboard/dashboards/identity/roles/tests.py
  54. 4
      openstack_dashboard/dashboards/identity/roles/urls.py
  55. 33
      openstack_dashboard/dashboards/identity/roles/views.py
  56. 0
      openstack_dashboard/dashboards/identity/users/__init__.py
  57. 2
      openstack_dashboard/dashboards/identity/users/forms.py
  58. 6
      openstack_dashboard/dashboards/identity/users/panel.py
  59. 4
      openstack_dashboard/dashboards/identity/users/tables.py
  60. 4
      openstack_dashboard/dashboards/identity/users/templates/users/_create.html
  61. 4
      openstack_dashboard/dashboards/identity/users/templates/users/_update.html
  62. 2
      openstack_dashboard/dashboards/identity/users/templates/users/create.html
  63. 0
      openstack_dashboard/dashboards/identity/users/templates/users/index.html
  64. 2
      openstack_dashboard/dashboards/identity/users/templates/users/update.html
  65. 8
      openstack_dashboard/dashboards/identity/users/tests.py
  66. 4
      openstack_dashboard/dashboards/identity/users/urls.py
  67. 46
      openstack_dashboard/dashboards/identity/users/views.py
  68. 8
      openstack_dashboard/enabled/_25_identity.py
  69. 3
      openstack_dashboard/policy.py
  70. 2
      openstack_dashboard/settings.py
  71. 3
      openstack_dashboard/test/settings.py

@ -61,6 +61,8 @@ class NotRegistered(Exception):
class HorizonComponent(object):
policy_rules = None
def __init__(self):
super(HorizonComponent, self).__init__()
if not self.slug:
@ -88,6 +90,29 @@ class HorizonComponent(object):
urlpatterns = patterns('')
return urlpatterns
def can_access(self, context):
"""Checks to see that the user has role based access to this component.
This method should be overridden to return the result of
any policy checks required for the user to access this component
when more complex checks are required.
"""
return self._can_access(context['request'])
def _can_access(self, request):
policy_check = getattr(settings, "POLICY_CHECK_FUNCTION", None)
# this check is an OR check rather than an AND check that is the
# default in the policy engine, so calling each rule individually
if policy_check and self.policy_rules:
for rule in self.policy_rules:
if policy_check((rule,), request):
return True
return False
# default to allowed
return True
class Registry(object):
def __init__(self):
@ -543,6 +568,30 @@ class Dashboard(Registry, HorizonComponent):
del loaders.panel_template_dirs[key]
return success
def can_access(self, context):
"""Checks for role based access for this dashboard.
Checks for access to any panels in the dashboard and of the the
dashboard itself.
This method should be overridden to return the result of
any policy checks required for the user to access this dashboard
when more complex checks are required.
"""
# if the dashboard has policy rules, honor those above individual
# panels
if not self._can_access(context['request']):
return False
# check if access is allowed to a single panel,
# the default for each panel is True
for panel in self.get_panels():
if panel.can_access(context):
return True
return False
class Workflow(object):
def __init__(*args, **kwargs):

@ -3,12 +3,16 @@
{% for heading, panels in components.iteritems %}
{% with panels|has_permissions_on_list:user as filtered_panels %}
{% if filtered_panels %}
{% if heading %}<h4>{{ heading }}</h4>{% endif %}
{% if accessible_panels %}
{% if heading %}<h4>{{ heading }}</h4>{% endif %}
{% endif %}
<ul class="main_nav">
{% for panel in filtered_panels %}
<li>
<a href="{{ panel.get_absolute_url }}" {% if current == panel.slug %}class="active"{% endif %} tabindex='1'>{{ panel.name }}</a>
</li>
{% if panel in accessible_panels or current == panel.slug %}
<li>
<a href="{{ panel.get_absolute_url }}" {% if current == panel.slug %}class="active"{% endif %} tabindex='1'>{{ panel.name }}</a>
</li>
{% endif %}
{% endfor %}
</ul>
{% endif %}

@ -53,15 +53,19 @@ def horizon_nav(context):
for group in panel_groups.values():
allowed_panels = []
for panel in group:
if callable(panel.nav) and panel.nav(context):
if callable(panel.nav) and panel.nav(context) and \
panel.can_access(context):
allowed_panels.append(panel)
elif not callable(panel.nav) and panel.nav:
elif not callable(panel.nav) and panel.nav and \
panel.can_access(context):
allowed_panels.append(panel)
if allowed_panels:
non_empty_groups.append((group.name, allowed_panels))
if callable(dash.nav) and dash.nav(context):
if callable(dash.nav) and dash.nav(context) and \
dash.can_access(context):
dashboards.append((dash, SortedDict(non_empty_groups)))
elif not callable(dash.nav) and dash.nav:
elif not callable(dash.nav) and dash.nav and \
dash.can_access(context):
dashboards.append((dash, SortedDict(non_empty_groups)))
return {'components': dashboards,
'user': context['request'].user,
@ -78,10 +82,11 @@ def horizon_main_nav(context):
current_dashboard = context['request'].horizon.get('dashboard', None)
dashboards = []
for dash in Horizon.get_dashboards():
if callable(dash.nav) and dash.nav(context):
dashboards.append(dash)
elif dash.nav:
dashboards.append(dash)
if dash.can_access(context['request']):
if callable(dash.nav) and dash.nav(context):
dashboards.append(dash)
elif dash.nav:
dashboards.append(dash)
return {'components': dashboards,
'user': context['request'].user,
'current': current_dashboard,
@ -100,9 +105,11 @@ def horizon_dashboard_nav(context):
for group in panel_groups.values():
allowed_panels = []
for panel in group:
if callable(panel.nav) and panel.nav(context):
if callable(panel.nav) and panel.nav(context) and \
panel.can_access(context):
allowed_panels.append(panel)
elif not callable(panel.nav) and panel.nav:
elif not callable(panel.nav) and panel.nav and \
panel.can_access(context):
allowed_panels.append(panel)
if allowed_panels:
non_empty_groups.append((group.name, allowed_panels))

@ -53,6 +53,19 @@ class AdminPanel(horizon.Panel):
urls = 'horizon.test.test_dashboards.cats.kittens.urls'
class RbacNoAccessPanel(horizon.Panel):
name = "RBAC Panel No"
slug = "rbac_panel_no"
def _can_access(self, request):
return False
class RbacYesAccessPanel(horizon.Panel):
name = "RBAC Panel Yes"
slug = "rbac_panel_yes"
class BaseHorizonTests(test.TestCase):
def setUp(self):
@ -439,3 +452,67 @@ class CustomPermissionsTests(BaseHorizonTests):
follow=False,
HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(resp.status_code, 200)
class RbacHorizonTests(test.TestCase):
def setUp(self):
super(RbacHorizonTests, self).setUp()
# Adjust our horizon config and register our custom dashboards/panels.
self.old_default_dash = settings.HORIZON_CONFIG['default_dashboard']
settings.HORIZON_CONFIG['default_dashboard'] = 'cats'
self.old_dashboards = settings.HORIZON_CONFIG['dashboards']
settings.HORIZON_CONFIG['dashboards'] = ('cats', 'dogs')
base.Horizon.register(Cats)
base.Horizon.register(Dogs)
Cats.register(RbacNoAccessPanel)
Cats.default_panel = 'rbac_panel_no'
Dogs.register(RbacYesAccessPanel)
Dogs.default_panel = 'rbac_panel_yes'
# Trigger discovery, registration, and URLconf generation if it
# hasn't happened yet.
base.Horizon._urls()
# Store our original dashboards
self._discovered_dashboards = base.Horizon._registry.keys()
# Gather up and store our original panels for each dashboard
self._discovered_panels = {}
for dash in self._discovered_dashboards:
panels = base.Horizon._registry[dash]._registry.keys()
self._discovered_panels[dash] = panels
def tearDown(self):
super(RbacHorizonTests, self).tearDown()
# Restore our settings
settings.HORIZON_CONFIG['default_dashboard'] = self.old_default_dash
settings.HORIZON_CONFIG['dashboards'] = self.old_dashboards
# Destroy our singleton and re-create it.
base.HorizonSite._instance = None
del base.Horizon
base.Horizon = base.HorizonSite()
# Reload the convenience references to Horizon stored in __init__
reload(import_module("horizon"))
# Re-register our original dashboards and panels.
# This is necessary because autodiscovery only works on the first
# import, and calling reload introduces innumerable additional
# problems. Manual re-registration is the only good way for testing.
self._discovered_dashboards.remove(Cats)
self._discovered_dashboards.remove(Dogs)
for dash in self._discovered_dashboards:
base.Horizon.register(dash)
for panel in self._discovered_panels[dash]:
dash.register(panel)
def test_rbac_panels(self):
context = {'request': None}
cats = horizon.get_dashboard("cats")
self.assertEqual(cats._registered_with, base.Horizon)
self.assertQuerysetEqual(cats.get_panels(),
['<Panel: rbac_panel_no>'])
self.assertFalse(cats.can_access(context))
dogs = horizon.get_dashboard("dogs")
self.assertEqual(dogs._registered_with, base.Horizon)
self.assertQuerysetEqual(dogs.get_panels(),
['<Panel: rbac_panel_yes>'])
self.assertTrue(dogs.can_access(context))

@ -252,8 +252,9 @@ def tenant_delete(request, project):
return manager.delete(project)
def tenant_list(request, paginate=False, marker=None, domain=None, user=None):
manager = VERSIONS.get_project_manager(request, admin=True)
def tenant_list(request, paginate=False, marker=None, domain=None, user=None,
admin=True):
manager = VERSIONS.get_project_manager(request, admin=admin)
page_size = utils.get_page_size(request)
limit = None

@ -25,16 +25,10 @@ class SystemPanels(horizon.PanelGroup):
'networks', 'routers', 'info')
class IdentityPanels(horizon.PanelGroup):
slug = "identity"
name = _("Identity")
panels = ('domains', 'projects', 'users', 'groups', 'roles')
class Admin(horizon.Dashboard):
name = _("Admin")
slug = "admin"
panels = (SystemPanels, IdentityPanels)
panels = (SystemPanels,)
default_panel = 'overview'
permissions = ('openstack.roles.admin',)

@ -1,23 +0,0 @@
# 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.
GROUPS_INDEX_URL = 'horizon:admin:groups:index'
GROUPS_INDEX_VIEW_TEMPLATE = 'admin/groups/index.html'
GROUPS_CREATE_URL = 'horizon:admin:groups:create'
GROUPS_CREATE_VIEW_TEMPLATE = 'admin/groups/create.html'
GROUPS_UPDATE_URL = 'horizon:admin:groups:update'
GROUPS_UPDATE_VIEW_TEMPLATE = 'admin/groups/update.html'
GROUPS_MANAGE_URL = 'horizon:admin:groups:manage_members'
GROUPS_MANAGE_VIEW_TEMPLATE = 'admin/groups/manage.html'
GROUPS_ADD_MEMBER_URL = 'horizon:admin:groups:add_members'
GROUPS_ADD_MEMBER_VIEW_TEMPLATE = 'admin/groups/add_non_member.html'
GROUPS_ADD_MEMBER_AJAX_VIEW_TEMPLATE = 'admin/groups/_add_non_member.html'

@ -0,0 +1,28 @@
# Copyright 2014 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
class Identity(horizon.Dashboard):
name = _("Identity")
slug = "identity"
default_panel = 'projects'
panels = ('domains', 'projects', 'users', 'groups', 'roles',)
horizon.register(Identity)

@ -15,8 +15,8 @@
DOMAIN_INFO_FIELDS = ("name",
"description",
"enabled")
DOMAINS_INDEX_URL = 'horizon:admin:domains:index'
DOMAINS_INDEX_VIEW_TEMPLATE = 'admin/domains/index.html'
DOMAINS_CREATE_URL = 'horizon:admin:domains:create'
DOMAINS_UPDATE_URL = 'horizon:admin:domains:update'
DOMAINS_INDEX_URL = 'horizon:identity:domains:index'
DOMAINS_INDEX_VIEW_TEMPLATE = 'identity/domains/index.html'
DOMAINS_CREATE_URL = 'horizon:identity:domains:create'
DOMAINS_UPDATE_URL = 'horizon:identity:domains:update'
DOMAIN_GROUP_MEMBER_SLUG = "update_group_members"

@ -17,13 +17,15 @@ from django.utils.translation import ugettext_lazy as _
import horizon
from openstack_dashboard.api import keystone
from openstack_dashboard.dashboards.admin import dashboard
from openstack_dashboard.dashboards.identity import dashboard
class Domains(horizon.Panel):
name = _("Domains")
slug = 'domains'
policy_rules = (("identity", "identity:get_domain"),
("identity", "identity:list_domains"))
if keystone.VERSIONS.active >= 3:
dashboard.Admin.register(Domains)
dashboard.Identity.register(Domains)

@ -26,7 +26,7 @@ from horizon import tables
from openstack_dashboard import api
from openstack_dashboard.dashboards.admin.domains import constants
from openstack_dashboard.dashboards.identity.domains import constants
LOG = logging.getLogger(__name__)
@ -35,7 +35,7 @@ LOG = logging.getLogger(__name__)
class ViewGroupsLink(tables.LinkAction):
name = "groups"
verbose_name = _("Modify Groups")
url = "horizon:admin:domains:update"
url = "horizon:identity:domains:update"
classes = ("ajax-modal",)
icon = "pencil"

@ -24,8 +24,8 @@ from horizon.workflows import views
from openstack_dashboard import api
from openstack_dashboard.test import helpers as test
from openstack_dashboard.dashboards.admin.domains import constants
from openstack_dashboard.dashboards.admin.domains import workflows
from openstack_dashboard.dashboards.identity.domains import constants
from openstack_dashboard.dashboards.identity.domains import workflows
DOMAINS_INDEX_URL = reverse(constants.DOMAINS_INDEX_URL)
@ -134,7 +134,7 @@ class CreateDomainWorkflowTests(test.BaseAdminViewTests):
return domain_info
def test_add_domain_get(self):
url = reverse('horizon:admin:domains:create')
url = reverse('horizon:identity:domains:create')
res = self.client.get(url)
self.assertTemplateUsed(res, views.WorkflowView.template_name)

@ -15,7 +15,7 @@
from django.conf.urls import patterns # noqa
from django.conf.urls import url # noqa
from openstack_dashboard.dashboards.admin.domains import views
from openstack_dashboard.dashboards.identity.domains import views
urlpatterns = patterns('',

@ -16,15 +16,17 @@ from django.core.urlresolvers import reverse
from django.utils.translation import ugettext_lazy as _
from horizon import exceptions
from horizon import messages
from horizon import tables
from horizon import workflows
from openstack_dashboard import api
from openstack_dashboard import policy
from openstack_dashboard.dashboards.admin.domains import constants
from openstack_dashboard.dashboards.admin.domains \
from openstack_dashboard.dashboards.identity.domains import constants
from openstack_dashboard.dashboards.identity.domains \
import tables as project_tables
from openstack_dashboard.dashboards.admin.domains \
from openstack_dashboard.dashboards.identity.domains \
import workflows as project_workflows
@ -35,16 +37,30 @@ class IndexView(tables.DataTableView):
def get_data(self):
domains = []
domain_context = self.request.session.get('domain_context', None)
try:
if domain_context:
if policy.check((("identity", "identity:list_domains"),),
self.request):
try:
if domain_context:
domain = api.keystone.domain_get(self.request,
domain_context)
domains.append(domain)
else:
domains = api.keystone.domain_list(self.request)
except Exception:
exceptions.handle(self.request,
_('Unable to retrieve domain list.'))
elif policy.check((("identity", "identity:get_domain"),),
self.request):
try:
domain = api.keystone.domain_get(self.request,
domain_context)
self.request.user.domain_id)
domains.append(domain)
else:
domains = api.keystone.domain_list(self.request)
except Exception:
exceptions.handle(self.request,
_('Unable to retrieve domain list.'))
except Exception:
exceptions.handle(self.request,
_('Unable to retrieve domain information.'))
else:
msg = _("Insufficient privilege level to view domain information.")
messages.info(self.request, msg)
return domains

@ -24,7 +24,7 @@ from horizon import workflows
from openstack_dashboard import api
from openstack_dashboard.dashboards.admin.domains import constants
from openstack_dashboard.dashboards.identity.domains import constants
LOG = logging.getLogger(__name__)

@ -0,0 +1,23 @@
# 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.
GROUPS_INDEX_URL = 'horizon:identity:groups:index'
GROUPS_INDEX_VIEW_TEMPLATE = 'identity/groups/index.html'
GROUPS_CREATE_URL = 'horizon:identity:groups:create'
GROUPS_CREATE_VIEW_TEMPLATE = 'identity/groups/create.html'
GROUPS_UPDATE_URL = 'horizon:identity:groups:update'
GROUPS_UPDATE_VIEW_TEMPLATE = 'identity/groups/update.html'
GROUPS_MANAGE_URL = 'horizon:identity:groups:manage_members'
GROUPS_MANAGE_VIEW_TEMPLATE = 'identity/groups/manage.html'
GROUPS_ADD_MEMBER_URL = 'horizon:identity:groups:add_members'
GROUPS_ADD_MEMBER_VIEW_TEMPLATE = 'identity/groups/add_non_member.html'
GROUPS_ADD_MEMBER_AJAX_VIEW_TEMPLATE = 'identity/groups/_add_non_member.html'

@ -17,13 +17,14 @@ from django.utils.translation import ugettext_lazy as _
import horizon
from openstack_dashboard.api import keystone
from openstack_dashboard.dashboards.admin import dashboard
from openstack_dashboard.dashboards.identity import dashboard
class Groups(horizon.Panel):
name = _("Groups")
slug = 'groups'
policy_rules = (("identity", "identity:list_groups"),)
if keystone.VERSIONS.active >= 3:
dashboard.Admin.register(Groups)
dashboard.Identity.register(Groups)

@ -22,7 +22,7 @@ from horizon import tables
from openstack_dashboard import api
from openstack_dashboard.dashboards.admin.groups import constants
from openstack_dashboard.dashboards.identity.groups import constants
LOG = logging.getLogger(__name__)

@ -5,5 +5,5 @@
{% block modal-header %}{% trans "Add Group Assignment" %}{% endblock %}
{% block modal-footer %}
<a href="{% url 'horizon:admin:groups:manage_members' group.id %}" class="btn btn-default secondary cancel close">{% trans "Cancel" %}</a>
<a href="{% url 'horizon:identity:groups:manage_members' group.id %}" class="btn btn-default secondary cancel close">{% trans "Cancel" %}</a>
{% endblock %}

@ -3,7 +3,7 @@
{% load url from future %}
{% block form_id %}create_group_form{% endblock %}
{% block form_action %}{% url 'horizon:admin:groups:create' %}{% endblock %}
{% block form_action %}{% url 'horizon:identity:groups:create' %}{% endblock %}
{% block modal-header %}{% trans "Create Group" %}{% endblock %}
@ -21,5 +21,5 @@
{% block modal-footer %}
<input class="btn btn-primary pull-right" type="submit" value="{% trans "Create Group" %}" />
<a href="{% url 'horizon:admin:groups:index' %}" class="btn btn-default secondary cancel close">{% trans "Cancel" %}</a>
<a href="{% url 'horizon:identity:groups:index' %}" class="btn btn-default secondary cancel close">{% trans "Cancel" %}</a>
{% endblock %}

@ -3,7 +3,7 @@
{% load url from future %}
{% block form_id %}update_group_form{% endblock %}
{% block form_action %}{% url 'horizon:admin:groups:update' group.id %}{% endblock %}
{% block form_action %}{% url 'horizon:identity:groups:update' group.id %}{% endblock %}
{% block modal-header %}{% trans "Update Group" %}{% endblock %}
@ -21,5 +21,5 @@
{% block modal-footer %}
<input class="btn btn-primary pull-right" type="submit" value="{% trans "Update Group" %}" />
<a href="{% url 'horizon:admin:groups:index' %}" class="btn btn-default secondary cancel close">{% trans "Cancel" %}</a>
<a href="{% url 'horizon:identity:groups:index' %}" class="btn btn-default secondary cancel close">{% trans "Cancel" %}</a>
{% endblock %}

@ -3,5 +3,5 @@
{% block title %}{% trans 'Add User to Group' %}{% endblock %}
{% block main %}
{% include 'admin/groups/_add_non_member.html' %}
{% include 'identity/groups/_add_non_member.html' %}
{% endblock %}

@ -7,5 +7,5 @@
{% endblock page_header %}
{% block main %}
{% include 'admin/groups/_create.html' %}
{% include 'identity/groups/_create.html' %}
{% endblock %}

@ -7,5 +7,5 @@
{% endblock page_header %}
{% block main %}
{% include 'admin/groups/_update.html' %}
{% include 'identity/groups/_update.html' %}
{% endblock %}

@ -21,7 +21,7 @@ from mox import IsA # noqa
from openstack_dashboard import api
from openstack_dashboard.test import helpers as test
from openstack_dashboard.dashboards.admin.groups import constants
from openstack_dashboard.dashboards.identity.groups import constants
GROUPS_INDEX_URL = reverse(constants.GROUPS_INDEX_URL)

@ -15,7 +15,7 @@
from django.conf.urls import patterns # noqa
from django.conf.urls import url # noqa
from openstack_dashboard.dashboards.admin.groups import views
from openstack_dashboard.dashboards.identity.groups import views
urlpatterns = patterns('',

@ -18,15 +18,17 @@ 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.admin.groups import constants
from openstack_dashboard.dashboards.admin.groups \
from openstack_dashboard.dashboards.identity.groups import constants
from openstack_dashboard.dashboards.identity.groups \
import forms as project_forms
from openstack_dashboard.dashboards.admin.groups \
from openstack_dashboard.dashboards.identity.groups \
import tables as project_tables
@ -37,12 +39,17 @@ class IndexView(tables.DataTableView):
def get_data(self):
groups = []
domain_context = self.request.session.get('domain_context', None)
try:
groups = api.keystone.group_list(self.request,
domain=domain_context)
except Exception:
exceptions.handle(self.request,
_('Unable to retrieve group list.'))
if policy.check((("identity", "identity:list_groups"),),
self.request):
try:
groups = api.keystone.group_list(self.request,
domain=domain_context)
except Exception:
exceptions.handle(self.request,
_('Unable to retrieve group list.'))
else:
msg = _("Insufficient privilege level to view group information.")
messages.info(self.request, msg)
return groups

@ -20,12 +20,14 @@ from django.utils.translation import ugettext_lazy as _
import horizon
from openstack_dashboard.dashboards.admin import dashboard
from openstack_dashboard.dashboards.identity import dashboard
class Tenants(horizon.Panel):
name = _("Projects")
slug = 'projects'
policy_rules = (("identity", "identity:list_projects"),
("identity", "identity:list_user_projects"))
dashboard.Admin.register(Tenants)
dashboard.Identity.register(Tenants)

@ -21,13 +21,13 @@ from horizon import tables
from keystoneclient.exceptions import Conflict # noqa
from openstack_dashboard import api
from openstack_dashboard.api import keystone
from openstack_dashboard import policy
class ViewMembersLink(tables.LinkAction):
name = "users"
verbose_name = _("Modify Users")
url = "horizon:admin:projects:update"
url = "horizon:identity:projects:update"
classes = ("ajax-modal",)
icon = "pencil"
policy_rules = (("identity", "identity:list_users"),
@ -43,12 +43,13 @@ class ViewMembersLink(tables.LinkAction):
class ViewGroupsLink(tables.LinkAction):
name = "groups"
verbose_name = _("Modify Groups")
url = "horizon:admin:projects:update"
url = "horizon:identity:projects:update"
classes = ("ajax-modal",)
icon = "pencil"
policy_rules = (("identity", "identity:list_groups"),)
def allowed(self, request, project):
return keystone.VERSIONS.active >= 3
return api.keystone.VERSIONS.active >= 3
def get_link_url(self, project):
step = 'update_group_members'
@ -60,15 +61,18 @@ class ViewGroupsLink(tables.LinkAction):
class UsageLink(tables.LinkAction):
name = "usage"
verbose_name = _("View Usage")
url = "horizon:admin:projects:usage"
url = "horizon:identity:projects:usage"
icon = "stats"
policy_rules = (("compute", "compute_extension:simple_tenant_usage:show"),)
def allowed(self, request, project):
return request.user.is_superuser
class CreateProject(tables.LinkAction):
name = "create"
verbose_name = _("Create Project")
url = "horizon:admin:projects:create"
url = "horizon:identity:projects:create"
classes = ("ajax-modal",)
icon = "plus"
policy_rules = (('identity', 'identity:create_project'),)
@ -80,7 +84,7 @@ class CreateProject(tables.LinkAction):
class UpdateProject(tables.LinkAction):
name = "update"
verbose_name = _("Edit Project")
url = "horizon:admin:projects:update"
url = "horizon:identity:projects:update"
classes = ("ajax-modal",)
icon = "pencil"
policy_rules = (('identity', 'identity:update_project'),)
@ -92,7 +96,7 @@ class UpdateProject(tables.LinkAction):
class ModifyQuotas(tables.LinkAction):
name = "quotas"
verbose_name = _("Modify Quotas")
url = "horizon:admin:projects:update"
url = "horizon:identity:projects:update"
classes = ("ajax-modal",)
icon = "pencil"
policy_rules = (('compute', "compute_extension:quotas:update"),)
@ -141,7 +145,9 @@ class UpdateRow(tables.Row):
class UpdateCell(tables.UpdateAction):
def allowed(self, request, project, cell):
return api.keystone.keystone_can_edit_project()
return api.keystone.keystone_can_edit_project() and \
policy.check((("identity", "identity:update_project"),),
request)
def update_cell(self, request, datum, project_id,
cell_name, new_cell_value):

@ -31,7 +31,7 @@ from horizon import exceptions
from horizon.workflows import views
from openstack_dashboard import api
from openstack_dashboard.dashboards.admin.projects import workflows
from openstack_dashboard.dashboards.identity.projects import workflows
from openstack_dashboard.test import helpers as test
from openstack_dashboard import usage
from openstack_dashboard.usage import quotas
@ -44,22 +44,23 @@ if with_sel:
from socket import timeout as socket_timeout # noqa
INDEX_URL = reverse('horizon:admin:projects:index')
INDEX_URL = reverse('horizon:identity:projects:index')
USER_ROLE_PREFIX = workflows.PROJECT_GROUP_MEMBER_SLUG + "_role_"
GROUP_ROLE_PREFIX = workflows.PROJECT_USER_MEMBER_SLUG + "_role_"
@test.create_stubs({api.keystone: ('tenant_list',)})
class TenantsViewTests(test.BaseAdminViewTests):
@test.create_stubs({api.keystone: ('tenant_list',)})
def test_index(self):
api.keystone.tenant_list(IsA(http.HttpRequest),
domain=None,
paginate=True) \
paginate=True,
marker=None) \
.AndReturn([self.tenants.list(), False])
self.mox.ReplayAll()
res = self.client.get(INDEX_URL)
self.assertTemplateUsed(res, 'admin/projects/index.html')
self.assertTemplateUsed(res, 'identity/projects/index.html')
self.assertItemsEqual(res.context['table'].data, self.tenants.list())
@test.create_stubs({api.keystone: ('tenant_list', )})
@ -70,16 +71,34 @@ class TenantsViewTests(test.BaseAdminViewTests):
domain_tenants = [tenant for tenant in self.tenants.list()
if tenant.domain_id == domain.id]
api.keystone.tenant_list(IsA(http.HttpRequest),
domain=domain.id) \
.AndReturn(domain_tenants)
domain=domain.id,
paginate=True,
marker=None) \
.AndReturn([domain_tenants, False])
self.mox.ReplayAll()
res = self.client.get(INDEX_URL)
self.assertTemplateUsed(res, 'admin/projects/index.html')
self.assertTemplateUsed(res, 'identity/projects/index.html')
self.assertItemsEqual(res.context['table'].data, domain_tenants)
self.assertContains(res, "<em>test_domain:</em>")
class ProjectsViewNonAdminTests(test.TestCase):
@test.create_stubs({api.keystone: ('tenant_list',)})
def test_index(self):
api.keystone.tenant_list(IsA(http.HttpRequest),
user=self.user.id,
paginate=True,
marker=None,
admin=False) \
.AndReturn([self.tenants.list(), False])
self.mox.ReplayAll()
res = self.client.get(INDEX_URL)
self.assertTemplateUsed(res, 'identity/projects/index.html')
self.assertItemsEqual(res.context['table'].data, self.tenants.list())
class CreateProjectWorkflowTests(test.BaseAdminViewTests):
def _get_project_info(self, project):
domain = self._get_default_domain()
@ -179,7 +198,7 @@ class CreateProjectWorkflowTests(test.BaseAdminViewTests):
self.mox.ReplayAll()
url = reverse('horizon:admin:projects:create')
url = reverse('horizon:identity:projects:create')
res = self.client.get(url)
self.assertTemplateUsed(res, views.WorkflowView.template_name)
@ -240,7 +259,7 @@ class CreateProjectWorkflowTests(test.BaseAdminViewTests):
.AndReturn(self.roles.list())
self.mox.ReplayAll()
res = self.client.get(reverse('horizon:admin:projects:create'))
res = self.client.get(reverse('horizon:identity:projects:create'))
self.assertTemplateUsed(res, views.WorkflowView.template_name)
if django.VERSION >= (1, 6):
@ -343,7 +362,7 @@ class CreateProjectWorkflowTests(test.BaseAdminViewTests):
workflow_data.update(self._get_workflow_data(project, quota))
url = reverse('horizon:admin:projects:create')
url = reverse('horizon:identity:projects:create')
res = self.client.post(url, workflow_data)
self.assertNoFormErrors(res)
@ -406,7 +425,7 @@ class CreateProjectWorkflowTests(test.BaseAdminViewTests):
self.mox.ReplayAll()
url = reverse('horizon:admin:projects:create')
url = reverse('horizon:identity:projects:create')
res = self.client.get(url)
self.assertTemplateUsed(res, views.WorkflowView.template_name)
@ -462,7 +481,7 @@ class CreateProjectWorkflowTests(test.BaseAdminViewTests):
workflow_data = self._get_workflow_data(project, quota)
url = reverse('horizon:admin:projects:create')
url = reverse('horizon:identity:projects:create')
res = self.client.post(url, workflow_data)
self.assertNoFormErrors(res)
@ -546,7 +565,7 @@ class CreateProjectWorkflowTests(test.BaseAdminViewTests):
workflow_data.update(self._get_workflow_data(project, quota))
url = reverse('horizon:admin:projects:create')
url = reverse('horizon:identity:projects:create')
res = self.client.post(url, workflow_data)
self.assertNoFormErrors(res)
@ -631,7 +650,7 @@ class CreateProjectWorkflowTests(test.BaseAdminViewTests):
workflow_data.update(self._get_workflow_data(project, quota))
url = reverse('horizon:admin:projects:create')
url = reverse('horizon:identity:projects:create')
res = self.client.post(url, workflow_data)
self.assertNoFormErrors(res)
@ -681,7 +700,7 @@ class CreateProjectWorkflowTests(test.BaseAdminViewTests):
workflow_data = self._get_workflow_data(project, quota)
workflow_data["name"] = ""
url = reverse('horizon:admin:projects:create')
url = reverse('horizon:identity:projects:create')
res = self.client.post(url, workflow_data)
self.assertContains(res, "field is required")
@ -796,7 +815,7 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests):
self.mox.ReplayAll()
url = reverse('horizon:admin:projects:update',
url = reverse('horizon:identity:projects:update',
args=[self.tenant.id])
res = self.client.get(url)
@ -1028,7 +1047,7 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests):
"enabled": project.enabled}
workflow_data.update(project_data)
workflow_data.update(updated_quota)
url = reverse('horizon:admin:projects:update',
url = reverse('horizon:identity:projects:update',
args=[self.tenant.id])
res = self.client.post(url, workflow_data)
@ -1064,7 +1083,7 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests):
self.mox.ReplayAll()
url = reverse('horizon:admin:projects:update',
url = reverse('horizon:identity:projects:update',
args=[self.tenant.id])
res = self.client.get(url)
@ -1180,7 +1199,7 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests):
"enabled": project.enabled}
workflow_data.update(project_data)
workflow_data.update(updated_quota)
url = reverse('horizon:admin:projects:update',
url = reverse('horizon:identity:projects:update',
args=[self.tenant.id])
res = self.client.post(url, workflow_data)
@ -1354,7 +1373,7 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests):
"enabled": project.enabled}
workflow_data.update(project_data)
workflow_data.update(updated_quota)
url = reverse('horizon:admin:projects:update',
url = reverse('horizon:identity:projects:update',
args=[self.tenant.id])
res = self.client.post(url, workflow_data)
@ -1488,7 +1507,7 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests):
"enabled": project.enabled}
workflow_data.update(project_data)
workflow_data.update(updated_quota)
url = reverse('horizon:admin:projects:update',
url = reverse('horizon:identity:projects:update',
args=[self.tenant.id])
res = self.client.post(url, workflow_data)
@ -1520,7 +1539,7 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests):
.AndReturn(quota)
self.mox.ReplayAll()
url = reverse('horizon:admin:projects:update',
url = reverse('horizon:identity:projects:update',
args=[self.tenant.id])
try:
@ -1586,7 +1605,7 @@ class UsageViewTests(test.BaseAdminViewTests):
self.mox.ReplayAll()
project_id = self.tenants.first().id
csv_url = reverse('horizon:admin:projects:usage',
csv_url = reverse('horizon:identity:projects:usage',
args=[project_id]) + "?format=csv"
res = self.client.get(csv_url)
self.assertTemplateUsed(res, 'project/overview/usage.csv')
@ -1639,7 +1658,7 @@ class SeleniumTests(test.SeleniumAdminTestCase):
# Check the presence of the important elements
td_element = self.selenium.find_element_by_xpath(
"//td[@data-update-url='/admin/projects/?action=cell_update"
"//td[@data-update-url='/identity/?action=cell_update"
"&table=tenants&cell_name=name&obj_id=1']")
cell_wrapper = td_element.find_element_by_class_name(
'table_cell_wrapper')
@ -1656,7 +1675,7 @@ class SeleniumTests(test.SeleniumAdminTestCase):
wait.until(lambda x: self.selenium.find_element_by_name("name__1"))
# Changing project name in cell form
td_element = self.selenium.find_element_by_xpath(
"//td[@data-update-url='/admin/projects/?action=cell_update"
"//td[@data-update-url='/identity/?action=cell_update"
"&table=tenants&cell_name=name&obj_id=1']")
name_input = td_element.find_element_by_tag_name('input')
name_input.send_keys(keys.Keys.HOME)
@ -1667,13 +1686,13 @@ class SeleniumTests(test.SeleniumAdminTestCase):
wait = self.ui.WebDriverWait(self.selenium, 10,
ignored_exceptions=[socket_timeout])
wait.until(lambda x: self.selenium.find_element_by_xpath(
"//td[@data-update-url='/admin/projects/?action=cell_update"
"//td[@data-update-url='/identity/?action=cell_update"
"&table=tenants&cell_name=name&obj_id=1']"
"/div[@class='table_cell_wrapper']"
"/div[@class='table_cell_data_wrapper']"))
# Checking new project name after cell refresh
data_wrapper = self.selenium.find_element_by_xpath(
"//td[@data-update-url='/admin/projects/?action=cell_update"
"//td[@data-update-url='/identity/?action=cell_update"
"&table=tenants&cell_name=name&obj_id=1']"
"/div[@class='table_cell_wrapper']"
"/div[@class='table_cell_data_wrapper']")
@ -1703,7 +1722,7 @@ class SeleniumTests(test.SeleniumAdminTestCase):
# Check the presence of the important elements
td_element = self.selenium.find_element_by_xpath(
"//td[@data-update-url='/admin/projects/?action=cell_update"
"//td[@data-update-url='/identity/?action=cell_update"
"&table=tenants&cell_name=name&obj_id=1']")
cell_wrapper = td_element.find_element_by_class_name(
'table_cell_wrapper')
@ -1720,13 +1739,13 @@ class SeleniumTests(test.SeleniumAdminTestCase):
wait.until(lambda x: self.selenium.find_element_by_name("name__1"))
# Click on cancel button
td_element = self.selenium.find_element_by_xpath(
"//td[@data-update-url='/admin/projects/?action=cell_update"
"//td[@data-update-url='/identity/?action=cell_update"
"&table=tenants&cell_name=name&obj_id=1']")
td_element.find_element_by_class_name('inline-edit-cancel').click()
# Cancel is via javascript, so it should be immediate
# Checking that tenant name is not changed
data_wrapper = self.selenium.find_element_by_xpath(
"//td[@data-update-url='/admin/projects/?action=cell_update"
"//td[@data-update-url='/identity/?action=cell_update"
"&table=tenants&cell_name=name&obj_id=1']"
"/div[@class='table_cell_wrapper']"
"/div[@class='table_cell_data_wrapper']")
@ -1768,7 +1787,7 @@ class SeleniumTests(test.SeleniumAdminTestCase):
self.mox.ReplayAll()
self.selenium.get("%s%s" % (self.live_server_url,
reverse('horizon:admin:projects:create')))
reverse('horizon:identity:projects:create')))
members = self.selenium.find_element_by_css_selector(member_css_class)

@ -19,7 +19,7 @@
from django.conf.urls import patterns # noqa
from django.conf.urls import url # noqa
from openstack_dashboard.dashboards.admin.projects import views
from openstack_dashboard.dashboards.identity.projects import views
urlpatterns = patterns('',

@ -20,18 +20,20 @@ from django.core.urlresolvers import reverse
from django.utils.translation import ugettext_lazy as _
from horizon import exceptions
from horizon import messages
from horizon import tables
from horizon.utils import memoized
from horizon import workflows
from openstack_dashboard import api
from openstack_dashboard.api import keystone
from openstack_dashboard import policy
from openstack_dashboard import usage
from openstack_dashboard.usage import quotas
from openstack_dashboard.dashboards.admin.projects \
from openstack_dashboard.dashboards.identity.projects \
import tables as project_tables
from openstack_dashboard.dashboards.admin.projects \
from openstack_dashboard.dashboards.identity.projects \
import workflows as project_workflows
from openstack_dashboard.dashboards.project.overview \
import views as project_views
@ -42,7 +44,7 @@ PROJECT_INFO_FIELDS = ("domain_id",
"description",
"enabled")
INDEX_URL = "horizon:admin:projects:index"
INDEX_URL = "horizon:identity:projects:index"
class TenantContextMixin(object):
@ -64,7 +66,7 @@ class TenantContextMixin(object):
class IndexView(tables.DataTableView):
table_class = project_tables.TenantsTable
template_name = 'admin/projects/index.html'