Merge "Separating Identity Dashboard and using RBAC"

This commit is contained in:
Jenkins 2014-08-19 13:30:31 +00:00 committed by Gerrit Code Review
commit 6393dbe003
71 changed files with 492 additions and 218 deletions

View File

@ -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):

View File

@ -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 %}

View File

@ -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))

View File

@ -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))

View File

@ -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

View File

@ -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',)

View File

@ -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'

View File

@ -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)

View File

@ -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"

View File

@ -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)

View File

@ -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"

View File

@ -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)

View File

@ -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('',

View File

@ -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

View File

@ -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__)

View File

@ -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'

View File

@ -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)

View File

@ -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__)

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

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

View File

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

View File

@ -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)

View File

@ -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('',

View File

@ -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

View File

@ -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)

View File

@ -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):

View File

@ -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)

View File

@ -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('',

View File

@ -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'
template_name = 'identity/projects/index.html'
def has_more_data(self, table):
return self._more
@ -74,23 +76,43 @@ class IndexView(tables.DataTableView):
marker = self.request.GET.get(
project_tables.TenantsTable._meta.pagination_param, None)
domain_context = self.request.session.get('domain_context', None)
try:
tenants, self._more = api.keystone.tenant_list(
self.request,
domain=domain_context,
paginate=True,
marker=marker)
except Exception:
if policy.check((("identity", "identity:list_projects"),),
self.request):
try:
tenants, self._more = api.keystone.tenant_list(
self.request,
domain=domain_context,
paginate=True,
marker=marker)
except Exception:
self._more = False
exceptions.handle(self.request,
_("Unable to retrieve project list."))
elif policy.check((("identity", "identity:list_user_projects"),),
self.request):
try:
tenants, self._more = api.keystone.tenant_list(
self.request,
user=self.request.user.id,
paginate=True,
marker=marker,
admin=False)
except Exception:
self._more = False
exceptions.handle(self.request,
_("Unable to retrieve project information."))
else:
self._more = False
exceptions.handle(self.request,
_("Unable to retrieve project list."))
msg = \
_("Insufficient privilege level to view project information.")
messages.info(self.request, msg)
return tenants
class ProjectUsageView(usage.UsageView):
table_class = usage.ProjectUsageTable
usage_class = usage.ProjectUsage
template_name = 'admin/projects/usage.html'
template_name = 'identity/projects/usage.html'
csv_response_class = project_views.ProjectUsageCsvRenderer
csv_template_name = 'project/overview/usage.csv'

View File

@ -33,8 +33,8 @@ from openstack_dashboard.api import keystone
from openstack_dashboard.api import nova
from openstack_dashboard.usage import quotas
INDEX_URL = "horizon:admin:projects:index"
ADD_USER_URL = "horizon:admin:projects:create_user"
INDEX_URL = "horizon:identity:projects:index"
ADD_USER_URL = "horizon:identity:projects:create_user"
PROJECT_GROUP_ENABLED = keystone.VERSIONS.active >= 3
PROJECT_USER_MEMBER_SLUG = "update_members"
PROJECT_GROUP_MEMBER_SLUG = "update_group_members"
@ -340,7 +340,7 @@ class CreateProject(workflows.Workflow):
finalize_button_name = _("Create Project")
success_message = _('Created new project "%s".')
failure_message = _('Unable to create project "%s".')
success_url = "horizon:admin:projects:index"
success_url = "horizon:identity:projects:index"
default_steps = (CreateProjectInfo,
UpdateProjectMembers,
UpdateProjectQuota)
@ -493,7 +493,7 @@ class UpdateProject(workflows.Workflow):
finalize_button_name = _("Save")
success_message = _('Modified project "%s".')
failure_message = _('Unable to modify project "%s".')
success_url = "horizon:admin:projects:index"
success_url = "horizon:identity:projects:index"
default_steps = (UpdateProjectInfo,
UpdateProjectMembers,
UpdateProjectQuota)

View File

@ -17,12 +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 Roles(horizon.Panel):
name = _("Roles")
slug = 'roles'
policy_rules = (("identity", "identity:list_roles"),)
if keystone.VERSIONS.active >= 3:
dashboard.Admin.register(Roles)
dashboard.Identity.register(Roles)

View File

@ -22,7 +22,7 @@ from openstack_dashboard import api
class CreateRoleLink(tables.LinkAction):
name = "create"
verbose_name = _("Create Role")
url = "horizon:admin:roles:create"
url = "horizon:identity:roles:create"
classes = ("ajax-modal",)
icon = "plus"
policy_rules = (("identity", "identity:create_role"),)
@ -34,7 +34,7 @@ class CreateRoleLink(tables.LinkAction):
class EditRoleLink(tables.LinkAction):
name = "edit"
verbose_name = _("Edit")
url = "horizon:admin:roles:update"
url = "horizon:identity:roles:update"
classes = ("ajax-modal",)
icon = "pencil"
policy_rules = (("identity", "identity:update_role"),)

View File

@ -3,7 +3,7 @@
{% load url from future %}
{% block form_id %}create_role_form{% endblock %}
{% block form_action %}{% url 'horizon:admin:roles:create' %}{% endblock %}
{% block form_action %}{% url 'horizon:identity:roles:create' %}{% endblock %}
{% block modal-header %}{% trans "Create Role" %}{% endblock %}
@ -21,5 +21,5 @@
{% 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 btn-default secondary cancel close">{% trans "Cancel" %}</a>
<a href="{% url 'horizon:identity:roles:index' %}" class="btn btn-default secondary cancel close">{% trans "Cancel" %}</a>
{% endblock %}

View File

@ -3,7 +3,7 @@
{% load url from future %}
{% block form_id %}update_role_form{% endblock %}
{% block form_action %}{% url 'horizon:admin:roles:update' role.id %}{% endblock %}
{% block form_action %}{% url 'horizon:identity:roles:update' role.id %}{% endblock %}
{% block modal-header %}{% trans "Update Role" %}{% endblock %}
@ -21,5 +21,5 @@
{% 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 btn-default secondary cancel close">{% trans "Cancel" %}</a>
<a href="{% url 'horizon:identity:roles:index' %}" class="btn btn-default secondary cancel close">{% trans "Cancel" %}</a>
{% endblock %}

View File

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

View File

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

View File

@ -22,9 +22,9 @@ 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])
ROLES_INDEX_URL = reverse('horizon:identity:roles:index')
ROLES_CREATE_URL = reverse('horizon:identity:roles:create')
ROLES_UPDATE_URL = reverse('horizon:identity:roles:update', args=[1])
class RolesViewTests(test.BaseAdminViewTests):
@ -39,7 +39,7 @@ class RolesViewTests(test.BaseAdminViewTests):
self.assertContains(res, 'Edit')
self.assertContains(res, 'Delete Role')
self.assertTemplateUsed(res, 'admin/roles/index.html')
self.assertTemplateUsed(res, 'identity/roles/index.html')
self.assertItemsEqual(res.context['table'].data, self.roles.list())
@test.create_stubs({api.keystone: ('role_list',
@ -56,7 +56,7 @@ class RolesViewTests(test.BaseAdminViewTests):
self.assertNotContains(res, 'Edit')
self.assertNotContains(res, 'Delete Role')
self.assertTemplateUsed(res, 'admin/roles/index.html')
self.assertTemplateUsed(res, 'identity/roles/index.html')
self.assertItemsEqual(res.context['table'].data, self.roles.list())
@test.create_stubs({api.keystone: ('role_create', )})

View File

@ -15,9 +15,9 @@
from django.conf.urls import patterns # noqa
from django.conf.urls import url # noqa
from openstack_dashboard.dashboards.admin.roles import views
from openstack_dashboard.dashboards.identity.roles import views
urlpatterns = patterns('openstack_dashboard.dashboards.admin.roles.views',
urlpatterns = patterns('openstack_dashboard.dashboards.identity.roles.views',
url(r'^$', views.IndexView.as_view(), name='index'),
url(r'^(?P<role_id>[^/]+)/update/$',
views.UpdateView.as_view(), name='update'),

View File

@ -18,42 +18,49 @@ 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.roles \
from openstack_dashboard.dashboards.identity.roles \
import forms as project_forms
from openstack_dashboard.dashboards.admin.roles \
from openstack_dashboard.dashboards.identity.roles \
import tables as project_tables
class IndexView(tables.DataTableView):
table_class = project_tables.RolesTable
template_name = 'admin/roles/index.html'
template_name = 'identity/roles/index.html'
def get_data(self):
roles = []
try:
roles = api.keystone.role_list(self.request)
except Exception:
exceptions.handle(self.request,
_('Unable to retrieve roles list.'))
if policy.check((("identity", "identity:list_roles"),),
self.request):
try:
roles = api.keystone.role_list(self.request)
except Exception:
exceptions.handle(self.request,
_('Unable to retrieve roles list.'))
else:
msg = _("Insufficient privilege level to view role information.")
messages.info(self.request, msg)
return roles
class UpdateView(forms.ModalFormView):
form_class = project_forms.UpdateRoleForm
template_name = 'admin/roles/update.html'
success_url = reverse_lazy('horizon:admin:roles:index')
template_name = 'identity/roles/update.html'
success_url = reverse_lazy('horizon:identity:roles:index')
@memoized.memoized_method
def get_object(self):
try:
return api.keystone.role_get(self.request, self.kwargs['role_id'])
except Exception:
redirect = reverse("horizon:admin:roles:index")
redirect = reverse("horizon:identity:roles:index")
exceptions.handle(self.request,
_('Unable to update role.'),
redirect=redirect)
@ -71,5 +78,5 @@ class UpdateView(forms.ModalFormView):
class CreateView(forms.ModalFormView):
form_class = project_forms.CreateRoleForm
template_name = 'admin/roles/create.html'
success_url = reverse_lazy('horizon:admin:roles:index')
template_name = 'identity/roles/create.html'
success_url = reverse_lazy('horizon:identity:roles:index')

View File

@ -67,7 +67,7 @@ class BaseUserForm(forms.SelfHandlingForm):
return data
ADD_PROJECT_URL = "horizon:admin:projects:create"
ADD_PROJECT_URL = "horizon:identity:projects:create"
class CreateUserForm(BaseUserForm):

View File

@ -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 Users(horizon.Panel):
name = _("Users")
slug = 'users'
policy_rules = (("identity", "identity:get_user"),
("identity", "identity:list_users"))
dashboard.Admin.register(Users)
dashboard.Identity.register(Users)

View File

@ -26,7 +26,7 @@ DISABLE = 1
class CreateUserLink(tables.LinkAction):
name = "create"
verbose_name = _("Create User")
url = "horizon:admin:users:create"
url = "horizon:identity:users:create"
classes = ("ajax-modal",)
icon = "plus"
policy_rules = (('identity', 'identity:create_grant'),
@ -41,7 +41,7 @@ class CreateUserLink(tables.LinkAction):
class EditUserLink(tables.LinkAction):
name = "edit"
verbose_name = _("Edit")
url = "horizon:admin:users:update"
url = "horizon:identity:users:update"
classes = ("ajax-modal",)
icon = "pencil"
policy_rules = (("identity", "identity:update_user"),

View File

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

View File

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

View File

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

View File

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

View File

@ -28,9 +28,9 @@ from openstack_dashboard import api
from openstack_dashboard.test import helpers as test
USERS_INDEX_URL = reverse('horizon:admin:users:index')
USER_CREATE_URL = reverse('horizon:admin:users:create')
USER_UPDATE_URL = reverse('horizon:admin:users:update', args=[1])
USERS_INDEX_URL = reverse('horizon:identity:users:index')
USER_CREATE_URL = reverse('horizon:identity:users:create')
USER_UPDATE_URL = reverse('horizon:identity:users:update', args=[1])
class UsersViewTests(test.BaseAdminViewTests):
@ -59,7 +59,7 @@ class UsersViewTests(test.BaseAdminViewTests):
self.mox.ReplayAll()
res = self.client.get(USERS_INDEX_URL)
self.assertTemplateUsed(res, 'admin/users/index.html')
self.assertTemplateUsed(res, 'identity/users/index.html')
self.assertItemsEqual(res.context['table'].data, users)
if domain_id:

View File

@ -19,9 +19,9 @@
from django.conf.urls import patterns # noqa
from django.conf.urls import url # noqa
from openstack_dashboard.dashboards.admin.users import views
from openstack_dashboard.dashboards.identity.users import views
urlpatterns = patterns('openstack_dashboard.dashboards.admin.users.views',
urlpatterns = patterns('openstack_dashboard.dashboards.identity.users.views',
url(r'^$', views.IndexView.as_view(), name='index'),
url(r'^(?P<user_id>[^/]+)/update/$',
views.UpdateView.as_view(), name='update'),

View File

@ -26,37 +26,53 @@ from django.views.decorators.debug import sensitive_post_parameters # noqa
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.users \
from openstack_dashboard.dashboards.identity.users \
import forms as project_forms
from openstack_dashboard.dashboards.admin.users \
from openstack_dashboard.dashboards.identity.users \
import tables as project_tables
class IndexView(tables.DataTableView):
table_class = project_tables.UsersTable
template_name = 'admin/users/index.html'
template_name = 'identity/users/index.html'
def get_data(self):
users = []
domain_context = self.request.session.get('domain_context', None)
try:
users = api.keystone.user_list(self.request,
domain=domain_context)
except Exception:
exceptions.handle(self.request,
_('Unable to retrieve user list.'))
if policy.check((("identity", "identity:list_users"),),
self.request):
try:
users = api.keystone.user_list(self.request,
domain=domain_context)
except Exception:
exceptions.handle(self.request,
_('Unable to retrieve user list.'))
elif policy.check((("identity", "identity:get_user"),),
self.request):
try:
user = api.keystone.user_get(self.request,
self.request.user.id)
users.append(user)
except Exception:
exceptions.handle(self.request,
_('Unable to retrieve user information.'))
else:
msg = _("Insufficient privilege level to view user information.")
messages.info(self.request, msg)
return users
class UpdateView(forms.ModalFormView):
form_class = project_forms.UpdateUserForm
template_name = 'admin/users/update.html'
success_url = reverse_lazy('horizon:admin:users:index')
template_name = 'identity/users/update.html'
success_url = reverse_lazy('horizon:identity:users:index')
@method_decorator(sensitive_post_parameters('password',
'confirm_password'))
@ -69,7 +85,7 @@ class UpdateView(forms.ModalFormView):
return api.keystone.user_get(self.request, self.kwargs['user_id'],
admin=True)
except Exception:
redirect = reverse("horizon:admin:users:index")
redirect = reverse("horizon:identity:users:index")
exceptions.handle(self.request,
_('Unable to update user.'),
redirect=redirect)
@ -102,8 +118,8 @@ class UpdateView(forms.ModalFormView):
class CreateView(forms.ModalFormView):
form_class = project_forms.CreateUserForm
template_name = 'admin/users/create.html'
success_url = reverse_lazy('horizon:admin:users:index')
template_name = 'identity/users/create.html'
success_url = reverse_lazy('horizon:identity:users:index')
@method_decorator(sensitive_post_parameters('password',
'confirm_password'))
@ -115,7 +131,7 @@ class CreateView(forms.ModalFormView):
try:
roles = api.keystone.role_list(self.request)
except Exception:
redirect = reverse("horizon:admin:users:index")
redirect = reverse("horizon:identity:users:index")
exceptions.handle(self.request,
_("Unable to retrieve user roles."),
redirect=redirect)

View File

@ -0,0 +1,8 @@
# The name of the dashboard to be added to HORIZON['dashboards']. Required.
DASHBOARD = 'identity'
# If set to True, this dashboard will be set as the default dashboard.
DEFAULT = False
# A dictionary of exception classes to be added to HORIZON['exceptions'].
ADD_EXCEPTIONS = {}
# A list of applications to be added to INSTALLED_APPS.
ADD_INSTALLED_APPS = ['openstack_dashboard.dashboards.identity']

View File

@ -110,6 +110,9 @@ def check(actions, request, target={}):
# same for user_id
if target.get('user_id') is None:
target['user_id'] = user.id
# same for domain_id
if target.get('domain_id') is None:
target['domain_id'] = user.domain_id
credentials = _user_to_credentials(request, user)

View File

@ -56,7 +56,7 @@ STATIC_URL = '/static/'
ROOT_URLCONF = 'openstack_dashboard.urls'
HORIZON_CONFIG = {
'dashboards': ('project', 'admin', 'settings', 'router',),
'dashboards': ('project', 'admin', 'router',),
'default_dashboard': 'project',
'user_home': 'openstack_dashboard.views.get_user_home',
'ajax_queue_limit': 10,

View File

@ -45,6 +45,7 @@ INSTALLED_APPS = (
'openstack_dashboard',
'openstack_dashboard.dashboards.project',
'openstack_dashboard.dashboards.admin',
'openstack_dashboard.dashboards.identity',
'openstack_dashboard.dashboards.settings',
'openstack_dashboard.dashboards.router',
)
@ -54,7 +55,7 @@ AUTHENTICATION_BACKENDS = ('openstack_auth.backend.KeystoneBackend',)
SITE_BRANDING = 'OpenStack'
HORIZON_CONFIG = {
'dashboards': ('project', 'admin', 'settings', 'router',),
'dashboards': ('project', 'admin', 'identity', 'settings', 'router',),
'default_dashboard': 'project',
"password_validator": {
"regex": '^.{8,18}$',