Separating Identity Dashboard and using RBAC

Moving identity panels to their own dashboard.

RBAC is now used to determine the data to load in the identity
dashboard. Using the default policy file, a user with role member
will now be able to see their project list.

Also, adding a policy check mechanism at the panel and dashboard
level to determine which panels and dashboards the user can access.

Implements blueprint separate-identity-dash

Change-Id: I7ebfec2bf6e44899bec79d3b23c90d56a976200f
This commit is contained in:
David Lyle
2014-02-05 12:08:15 -07:00
parent 8863146bb0
commit 18e8ea810d
71 changed files with 492 additions and 218 deletions

View File

@@ -61,6 +61,8 @@ class NotRegistered(Exception):
class HorizonComponent(object): class HorizonComponent(object):
policy_rules = None
def __init__(self): def __init__(self):
super(HorizonComponent, self).__init__() super(HorizonComponent, self).__init__()
if not self.slug: if not self.slug:
@@ -88,6 +90,29 @@ class HorizonComponent(object):
urlpatterns = patterns('') urlpatterns = patterns('')
return urlpatterns 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): class Registry(object):
def __init__(self): def __init__(self):
@@ -543,6 +568,30 @@ class Dashboard(Registry, HorizonComponent):
del loaders.panel_template_dirs[key] del loaders.panel_template_dirs[key]
return success 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): class Workflow(object):
def __init__(*args, **kwargs): def __init__(*args, **kwargs):

View File

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

View File

@@ -53,15 +53,19 @@ def horizon_nav(context):
for group in panel_groups.values(): for group in panel_groups.values():
allowed_panels = [] allowed_panels = []
for panel in group: 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) 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) allowed_panels.append(panel)
if allowed_panels: if allowed_panels:
non_empty_groups.append((group.name, 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))) 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))) dashboards.append((dash, SortedDict(non_empty_groups)))
return {'components': dashboards, return {'components': dashboards,
'user': context['request'].user, 'user': context['request'].user,
@@ -78,10 +82,11 @@ def horizon_main_nav(context):
current_dashboard = context['request'].horizon.get('dashboard', None) current_dashboard = context['request'].horizon.get('dashboard', None)
dashboards = [] dashboards = []
for dash in Horizon.get_dashboards(): for dash in Horizon.get_dashboards():
if callable(dash.nav) and dash.nav(context): if dash.can_access(context['request']):
dashboards.append(dash) if callable(dash.nav) and dash.nav(context):
elif dash.nav: dashboards.append(dash)
dashboards.append(dash) elif dash.nav:
dashboards.append(dash)
return {'components': dashboards, return {'components': dashboards,
'user': context['request'].user, 'user': context['request'].user,
'current': current_dashboard, 'current': current_dashboard,
@@ -100,9 +105,11 @@ def horizon_dashboard_nav(context):
for group in panel_groups.values(): for group in panel_groups.values():
allowed_panels = [] allowed_panels = []
for panel in group: 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) 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) allowed_panels.append(panel)
if allowed_panels: if allowed_panels:
non_empty_groups.append((group.name, 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' 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): class BaseHorizonTests(test.TestCase):
def setUp(self): def setUp(self):
@@ -439,3 +452,67 @@ class CustomPermissionsTests(BaseHorizonTests):
follow=False, follow=False,
HTTP_X_REQUESTED_WITH='XMLHttpRequest') HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(resp.status_code, 200) 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) return manager.delete(project)
def tenant_list(request, paginate=False, marker=None, domain=None, user=None): def tenant_list(request, paginate=False, marker=None, domain=None, user=None,
manager = VERSIONS.get_project_manager(request, admin=True) admin=True):
manager = VERSIONS.get_project_manager(request, admin=admin)
page_size = utils.get_page_size(request) page_size = utils.get_page_size(request)
limit = None limit = None

View File

@@ -25,16 +25,10 @@ class SystemPanels(horizon.PanelGroup):
'networks', 'routers', 'info') 'networks', 'routers', 'info')
class IdentityPanels(horizon.PanelGroup):
slug = "identity"
name = _("Identity")
panels = ('domains', 'projects', 'users', 'groups', 'roles')
class Admin(horizon.Dashboard): class Admin(horizon.Dashboard):
name = _("Admin") name = _("Admin")
slug = "admin" slug = "admin"
panels = (SystemPanels, IdentityPanels) panels = (SystemPanels,)
default_panel = 'overview' default_panel = 'overview'
permissions = ('openstack.roles.admin',) 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", DOMAIN_INFO_FIELDS = ("name",
"description", "description",
"enabled") "enabled")
DOMAINS_INDEX_URL = 'horizon:admin:domains:index' DOMAINS_INDEX_URL = 'horizon:identity:domains:index'
DOMAINS_INDEX_VIEW_TEMPLATE = 'admin/domains/index.html' DOMAINS_INDEX_VIEW_TEMPLATE = 'identity/domains/index.html'
DOMAINS_CREATE_URL = 'horizon:admin:domains:create' DOMAINS_CREATE_URL = 'horizon:identity:domains:create'
DOMAINS_UPDATE_URL = 'horizon:admin:domains:update' DOMAINS_UPDATE_URL = 'horizon:identity:domains:update'
DOMAIN_GROUP_MEMBER_SLUG = "update_group_members" DOMAIN_GROUP_MEMBER_SLUG = "update_group_members"

View File

@@ -17,13 +17,15 @@ from django.utils.translation import ugettext_lazy as _
import horizon import horizon
from openstack_dashboard.api import keystone from openstack_dashboard.api import keystone
from openstack_dashboard.dashboards.admin import dashboard from openstack_dashboard.dashboards.identity import dashboard
class Domains(horizon.Panel): class Domains(horizon.Panel):
name = _("Domains") name = _("Domains")
slug = 'domains' slug = 'domains'
policy_rules = (("identity", "identity:get_domain"),
("identity", "identity:list_domains"))
if keystone.VERSIONS.active >= 3: 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 import api
from openstack_dashboard.dashboards.admin.domains import constants from openstack_dashboard.dashboards.identity.domains import constants
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@@ -35,7 +35,7 @@ LOG = logging.getLogger(__name__)
class ViewGroupsLink(tables.LinkAction): class ViewGroupsLink(tables.LinkAction):
name = "groups" name = "groups"
verbose_name = _("Modify Groups") verbose_name = _("Modify Groups")
url = "horizon:admin:domains:update" url = "horizon:identity:domains:update"
classes = ("ajax-modal",) classes = ("ajax-modal",)
icon = "pencil" icon = "pencil"

View File

@@ -24,8 +24,8 @@ from horizon.workflows import views
from openstack_dashboard import api from openstack_dashboard import api
from openstack_dashboard.test import helpers as test from openstack_dashboard.test import helpers as test
from openstack_dashboard.dashboards.admin.domains import constants from openstack_dashboard.dashboards.identity.domains import constants
from openstack_dashboard.dashboards.admin.domains import workflows from openstack_dashboard.dashboards.identity.domains import workflows
DOMAINS_INDEX_URL = reverse(constants.DOMAINS_INDEX_URL) DOMAINS_INDEX_URL = reverse(constants.DOMAINS_INDEX_URL)
@@ -134,7 +134,7 @@ class CreateDomainWorkflowTests(test.BaseAdminViewTests):
return domain_info return domain_info
def test_add_domain_get(self): def test_add_domain_get(self):
url = reverse('horizon:admin:domains:create') url = reverse('horizon:identity:domains:create')
res = self.client.get(url) res = self.client.get(url)
self.assertTemplateUsed(res, views.WorkflowView.template_name) 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 patterns # noqa
from django.conf.urls import url # 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('', urlpatterns = patterns('',

View File

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

View File

@@ -24,7 +24,7 @@ from horizon import workflows
from openstack_dashboard import api 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__) 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 import horizon
from openstack_dashboard.api import keystone from openstack_dashboard.api import keystone
from openstack_dashboard.dashboards.admin import dashboard from openstack_dashboard.dashboards.identity import dashboard
class Groups(horizon.Panel): class Groups(horizon.Panel):
name = _("Groups") name = _("Groups")
slug = 'groups' slug = 'groups'
policy_rules = (("identity", "identity:list_groups"),)
if keystone.VERSIONS.active >= 3: 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 import api
from openstack_dashboard.dashboards.admin.groups import constants from openstack_dashboard.dashboards.identity.groups import constants
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)

View File

@@ -5,5 +5,5 @@
{% block modal-header %}{% trans "Add Group Assignment" %}{% endblock %} {% block modal-header %}{% trans "Add Group Assignment" %}{% endblock %}
{% block modal-footer %} {% 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 %} {% endblock %}

View File

@@ -3,7 +3,7 @@
{% load url from future %} {% load url from future %}
{% block form_id %}create_group_form{% endblock %} {% 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 %} {% block modal-header %}{% trans "Create Group" %}{% endblock %}
@@ -21,5 +21,5 @@
{% block modal-footer %} {% block modal-footer %}
<input class="btn btn-primary pull-right" type="submit" value="{% trans "Create Group" %}" /> <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 %} {% endblock %}

View File

@@ -3,7 +3,7 @@
{% load url from future %} {% load url from future %}
{% block form_id %}update_group_form{% endblock %} {% 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 %} {% block modal-header %}{% trans "Update Group" %}{% endblock %}
@@ -21,5 +21,5 @@
{% block modal-footer %} {% block modal-footer %}
<input class="btn btn-primary pull-right" type="submit" value="{% trans "Update Group" %}" /> <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 %} {% endblock %}

View File

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

View File

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

View File

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

View File

@@ -21,7 +21,7 @@ from mox import IsA # noqa
from openstack_dashboard import api from openstack_dashboard import api
from openstack_dashboard.test import helpers as test 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) 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 patterns # noqa
from django.conf.urls import url # 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('', urlpatterns = patterns('',

View File

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

View File

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

View File

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

View File

@@ -20,18 +20,20 @@ from django.core.urlresolvers import reverse
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from horizon import exceptions from horizon import exceptions
from horizon import messages
from horizon import tables from horizon import tables
from horizon.utils import memoized from horizon.utils import memoized
from horizon import workflows from horizon import workflows
from openstack_dashboard import api from openstack_dashboard import api
from openstack_dashboard.api import keystone from openstack_dashboard.api import keystone
from openstack_dashboard import policy
from openstack_dashboard import usage from openstack_dashboard import usage
from openstack_dashboard.usage import quotas from openstack_dashboard.usage import quotas
from openstack_dashboard.dashboards.admin.projects \ from openstack_dashboard.dashboards.identity.projects \
import tables as project_tables import tables as project_tables
from openstack_dashboard.dashboards.admin.projects \ from openstack_dashboard.dashboards.identity.projects \
import workflows as project_workflows import workflows as project_workflows
from openstack_dashboard.dashboards.project.overview \ from openstack_dashboard.dashboards.project.overview \
import views as project_views import views as project_views
@@ -42,7 +44,7 @@ PROJECT_INFO_FIELDS = ("domain_id",
"description", "description",
"enabled") "enabled")
INDEX_URL = "horizon:admin:projects:index" INDEX_URL = "horizon:identity:projects:index"
class TenantContextMixin(object): class TenantContextMixin(object):
@@ -64,7 +66,7 @@ class TenantContextMixin(object):
class IndexView(tables.DataTableView): class IndexView(tables.DataTableView):
table_class = project_tables.TenantsTable table_class = project_tables.TenantsTable
template_name = 'admin/projects/index.html' template_name = 'identity/projects/index.html'
def has_more_data(self, table): def has_more_data(self, table):
return self._more return self._more
@@ -74,23 +76,43 @@ class IndexView(tables.DataTableView):
marker = self.request.GET.get( marker = self.request.GET.get(
project_tables.TenantsTable._meta.pagination_param, None) project_tables.TenantsTable._meta.pagination_param, None)
domain_context = self.request.session.get('domain_context', None) domain_context = self.request.session.get('domain_context', None)
try: if policy.check((("identity", "identity:list_projects"),),
tenants, self._more = api.keystone.tenant_list( self.request):
self.request, try:
domain=domain_context, tenants, self._more = api.keystone.tenant_list(
paginate=True, self.request,
marker=marker) domain=domain_context,
except Exception: 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 self._more = False
exceptions.handle(self.request, msg = \
_("Unable to retrieve project list.")) _("Insufficient privilege level to view project information.")
messages.info(self.request, msg)
return tenants return tenants
class ProjectUsageView(usage.UsageView): class ProjectUsageView(usage.UsageView):
table_class = usage.ProjectUsageTable table_class = usage.ProjectUsageTable
usage_class = usage.ProjectUsage usage_class = usage.ProjectUsage
template_name = 'admin/projects/usage.html' template_name = 'identity/projects/usage.html'
csv_response_class = project_views.ProjectUsageCsvRenderer csv_response_class = project_views.ProjectUsageCsvRenderer
csv_template_name = 'project/overview/usage.csv' 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.api import nova
from openstack_dashboard.usage import quotas from openstack_dashboard.usage import quotas
INDEX_URL = "horizon:admin:projects:index" INDEX_URL = "horizon:identity:projects:index"
ADD_USER_URL = "horizon:admin:projects:create_user" ADD_USER_URL = "horizon:identity:projects:create_user"
PROJECT_GROUP_ENABLED = keystone.VERSIONS.active >= 3 PROJECT_GROUP_ENABLED = keystone.VERSIONS.active >= 3
PROJECT_USER_MEMBER_SLUG = "update_members" PROJECT_USER_MEMBER_SLUG = "update_members"
PROJECT_GROUP_MEMBER_SLUG = "update_group_members" PROJECT_GROUP_MEMBER_SLUG = "update_group_members"
@@ -340,7 +340,7 @@ class CreateProject(workflows.Workflow):
finalize_button_name = _("Create Project") finalize_button_name = _("Create Project")
success_message = _('Created new project "%s".') success_message = _('Created new project "%s".')
failure_message = _('Unable to create project "%s".') failure_message = _('Unable to create project "%s".')
success_url = "horizon:admin:projects:index" success_url = "horizon:identity:projects:index"
default_steps = (CreateProjectInfo, default_steps = (CreateProjectInfo,
UpdateProjectMembers, UpdateProjectMembers,
UpdateProjectQuota) UpdateProjectQuota)
@@ -493,7 +493,7 @@ class UpdateProject(workflows.Workflow):
finalize_button_name = _("Save") finalize_button_name = _("Save")
success_message = _('Modified project "%s".') success_message = _('Modified project "%s".')
failure_message = _('Unable to modify project "%s".') failure_message = _('Unable to modify project "%s".')
success_url = "horizon:admin:projects:index" success_url = "horizon:identity:projects:index"
default_steps = (UpdateProjectInfo, default_steps = (UpdateProjectInfo,
UpdateProjectMembers, UpdateProjectMembers,
UpdateProjectQuota) UpdateProjectQuota)

View File

@@ -17,12 +17,14 @@ from django.utils.translation import ugettext_lazy as _
import horizon import horizon
from openstack_dashboard.api import keystone from openstack_dashboard.api import keystone
from openstack_dashboard.dashboards.admin import dashboard from openstack_dashboard.dashboards.identity import dashboard
class Roles(horizon.Panel): class Roles(horizon.Panel):
name = _("Roles") name = _("Roles")
slug = 'roles' slug = 'roles'
policy_rules = (("identity", "identity:list_roles"),)
if keystone.VERSIONS.active >= 3: 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): class CreateRoleLink(tables.LinkAction):
name = "create" name = "create"
verbose_name = _("Create Role") verbose_name = _("Create Role")
url = "horizon:admin:roles:create" url = "horizon:identity:roles:create"
classes = ("ajax-modal",) classes = ("ajax-modal",)
icon = "plus" icon = "plus"
policy_rules = (("identity", "identity:create_role"),) policy_rules = (("identity", "identity:create_role"),)
@@ -34,7 +34,7 @@ class CreateRoleLink(tables.LinkAction):
class EditRoleLink(tables.LinkAction): class EditRoleLink(tables.LinkAction):
name = "edit" name = "edit"
verbose_name = _("Edit") verbose_name = _("Edit")
url = "horizon:admin:roles:update" url = "horizon:identity:roles:update"
classes = ("ajax-modal",) classes = ("ajax-modal",)
icon = "pencil" icon = "pencil"
policy_rules = (("identity", "identity:update_role"),) policy_rules = (("identity", "identity:update_role"),)

View File

@@ -3,7 +3,7 @@
{% load url from future %} {% load url from future %}
{% block form_id %}create_role_form{% endblock %} {% 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 %} {% block modal-header %}{% trans "Create Role" %}{% endblock %}
@@ -21,5 +21,5 @@
{% block modal-footer %} {% block modal-footer %}
<input class="btn btn-primary pull-right" type="submit" value="{% trans "Create Role" %}" /> <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 %} {% endblock %}

View File

@@ -3,7 +3,7 @@
{% load url from future %} {% load url from future %}
{% block form_id %}update_role_form{% endblock %} {% 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 %} {% block modal-header %}{% trans "Update Role" %}{% endblock %}
@@ -21,5 +21,5 @@
{% block modal-footer %} {% block modal-footer %}
<input class="btn btn-primary pull-right" type="submit" value="{% trans "Update Role" %}" /> <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 %} {% endblock %}

View File

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

View File

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

View File

@@ -22,9 +22,9 @@ from openstack_dashboard import api
from openstack_dashboard.test import helpers as test from openstack_dashboard.test import helpers as test
ROLES_INDEX_URL = reverse('horizon:admin:roles:index') ROLES_INDEX_URL = reverse('horizon:identity:roles:index')
ROLES_CREATE_URL = reverse('horizon:admin:roles:create') ROLES_CREATE_URL = reverse('horizon:identity:roles:create')
ROLES_UPDATE_URL = reverse('horizon:admin:roles:update', args=[1]) ROLES_UPDATE_URL = reverse('horizon:identity:roles:update', args=[1])
class RolesViewTests(test.BaseAdminViewTests): class RolesViewTests(test.BaseAdminViewTests):
@@ -39,7 +39,7 @@ class RolesViewTests(test.BaseAdminViewTests):
self.assertContains(res, 'Edit') self.assertContains(res, 'Edit')
self.assertContains(res, 'Delete Role') 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()) self.assertItemsEqual(res.context['table'].data, self.roles.list())
@test.create_stubs({api.keystone: ('role_list', @test.create_stubs({api.keystone: ('role_list',
@@ -56,7 +56,7 @@ class RolesViewTests(test.BaseAdminViewTests):
self.assertNotContains(res, 'Edit') self.assertNotContains(res, 'Edit')
self.assertNotContains(res, 'Delete Role') 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()) self.assertItemsEqual(res.context['table'].data, self.roles.list())
@test.create_stubs({api.keystone: ('role_create', )}) @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 patterns # noqa
from django.conf.urls import url # 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'^$', views.IndexView.as_view(), name='index'),
url(r'^(?P<role_id>[^/]+)/update/$', url(r'^(?P<role_id>[^/]+)/update/$',
views.UpdateView.as_view(), name='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 exceptions
from horizon import forms from horizon import forms
from horizon import messages
from horizon import tables from horizon import tables
from horizon.utils import memoized from horizon.utils import memoized
from openstack_dashboard import api 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 import forms as project_forms
from openstack_dashboard.dashboards.admin.roles \ from openstack_dashboard.dashboards.identity.roles \
import tables as project_tables import tables as project_tables
class IndexView(tables.DataTableView): class IndexView(tables.DataTableView):
table_class = project_tables.RolesTable table_class = project_tables.RolesTable
template_name = 'admin/roles/index.html' template_name = 'identity/roles/index.html'
def get_data(self): def get_data(self):
roles = [] roles = []
try: if policy.check((("identity", "identity:list_roles"),),
roles = api.keystone.role_list(self.request) self.request):
except Exception: try:
exceptions.handle(self.request, roles = api.keystone.role_list(self.request)
_('Unable to retrieve roles list.')) 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 return roles
class UpdateView(forms.ModalFormView): class UpdateView(forms.ModalFormView):
form_class = project_forms.UpdateRoleForm form_class = project_forms.UpdateRoleForm
template_name = 'admin/roles/update.html' template_name = 'identity/roles/update.html'
success_url = reverse_lazy('horizon:admin:roles:index') success_url = reverse_lazy('horizon:identity:roles:index')
@memoized.memoized_method @memoized.memoized_method
def get_object(self): def get_object(self):
try: try:
return api.keystone.role_get(self.request, self.kwargs['role_id']) return api.keystone.role_get(self.request, self.kwargs['role_id'])
except Exception: except Exception:
redirect = reverse("horizon:admin:roles:index") redirect = reverse("horizon:identity:roles:index")
exceptions.handle(self.request, exceptions.handle(self.request,
_('Unable to update role.'), _('Unable to update role.'),
redirect=redirect) redirect=redirect)
@@ -71,5 +78,5 @@ class UpdateView(forms.ModalFormView):
class CreateView(forms.ModalFormView): class CreateView(forms.ModalFormView):
form_class = project_forms.CreateRoleForm form_class = project_forms.CreateRoleForm
template_name = 'admin/roles/create.html' template_name = 'identity/roles/create.html'
success_url = reverse_lazy('horizon:admin:roles:index') success_url = reverse_lazy('horizon:identity:roles:index')

View File

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

View File

@@ -20,12 +20,14 @@ from django.utils.translation import ugettext_lazy as _
import horizon import horizon
from openstack_dashboard.dashboards.admin import dashboard from openstack_dashboard.dashboards.identity import dashboard
class Users(horizon.Panel): class Users(horizon.Panel):
name = _("Users") name = _("Users")
slug = '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): class CreateUserLink(tables.LinkAction):
name = "create" name = "create"
verbose_name = _("Create User") verbose_name = _("Create User")
url = "horizon:admin:users:create" url = "horizon:identity:users:create"
classes = ("ajax-modal",) classes = ("ajax-modal",)
icon = "plus" icon = "plus"
policy_rules = (('identity', 'identity:create_grant'), policy_rules = (('identity', 'identity:create_grant'),
@@ -41,7 +41,7 @@ class CreateUserLink(tables.LinkAction):
class EditUserLink(tables.LinkAction): class EditUserLink(tables.LinkAction):
name = "edit" name = "edit"
verbose_name = _("Edit") verbose_name = _("Edit")
url = "horizon:admin:users:update" url = "horizon:identity:users:update"
classes = ("ajax-modal",) classes = ("ajax-modal",)
icon = "pencil" icon = "pencil"
policy_rules = (("identity", "identity:update_user"), policy_rules = (("identity", "identity:update_user"),

View File

@@ -3,7 +3,7 @@
{% load url from future %} {% load url from future %}
{% block form_id %}create_user_form{% endblock %} {% 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 %} {% block modal-header %}{% trans "Create User" %}{% endblock %}
@@ -31,5 +31,5 @@
{% block modal-footer %} {% block modal-footer %}
<input class="btn btn-primary pull-right" type="submit" value="{% trans "Create User" %}" /> <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 %} {% endblock %}

View File

@@ -3,7 +3,7 @@
{% load url from future %} {% load url from future %}
{% block form_id %}update_user_form{% endblock %} {% 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 %} {% block modal-header %}{% trans "Update User" %}{% endblock %}
@@ -31,5 +31,5 @@
{% block modal-footer %} {% block modal-footer %}
<input class="btn btn-primary pull-right" type="submit" value="{% trans "Update User" %}" /> <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 %} {% endblock %}

View File

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

View File

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

View File

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

View File

@@ -19,9 +19,9 @@
from django.conf.urls import patterns # noqa from django.conf.urls import patterns # noqa
from django.conf.urls import url # 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'^$', views.IndexView.as_view(), name='index'),
url(r'^(?P<user_id>[^/]+)/update/$', url(r'^(?P<user_id>[^/]+)/update/$',
views.UpdateView.as_view(), name='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 exceptions
from horizon import forms from horizon import forms
from horizon import messages
from horizon import tables from horizon import tables
from horizon.utils import memoized from horizon.utils import memoized
from openstack_dashboard import api 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 import forms as project_forms
from openstack_dashboard.dashboards.admin.users \ from openstack_dashboard.dashboards.identity.users \
import tables as project_tables import tables as project_tables
class IndexView(tables.DataTableView): class IndexView(tables.DataTableView):
table_class = project_tables.UsersTable table_class = project_tables.UsersTable
template_name = 'admin/users/index.html' template_name = 'identity/users/index.html'
def get_data(self): def get_data(self):
users = [] users = []
domain_context = self.request.session.get('domain_context', None) domain_context = self.request.session.get('domain_context', None)
try: if policy.check((("identity", "identity:list_users"),),
users = api.keystone.user_list(self.request, self.request):
domain=domain_context) try:
except Exception: users = api.keystone.user_list(self.request,
exceptions.handle(self.request, domain=domain_context)
_('Unable to retrieve user list.')) 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 return users
class UpdateView(forms.ModalFormView): class UpdateView(forms.ModalFormView):
form_class = project_forms.UpdateUserForm form_class = project_forms.UpdateUserForm
template_name = 'admin/users/update.html' template_name = 'identity/users/update.html'
success_url = reverse_lazy('horizon:admin:users:index') success_url = reverse_lazy('horizon:identity:users:index')
@method_decorator(sensitive_post_parameters('password', @method_decorator(sensitive_post_parameters('password',
'confirm_password')) 'confirm_password'))
@@ -69,7 +85,7 @@ class UpdateView(forms.ModalFormView):
return api.keystone.user_get(self.request, self.kwargs['user_id'], return api.keystone.user_get(self.request, self.kwargs['user_id'],
admin=True) admin=True)
except Exception: except Exception:
redirect = reverse("horizon:admin:users:index") redirect = reverse("horizon:identity:users:index")
exceptions.handle(self.request, exceptions.handle(self.request,
_('Unable to update user.'), _('Unable to update user.'),
redirect=redirect) redirect=redirect)
@@ -102,8 +118,8 @@ class UpdateView(forms.ModalFormView):
class CreateView(forms.ModalFormView): class CreateView(forms.ModalFormView):
form_class = project_forms.CreateUserForm form_class = project_forms.CreateUserForm
template_name = 'admin/users/create.html' template_name = 'identity/users/create.html'
success_url = reverse_lazy('horizon:admin:users:index') success_url = reverse_lazy('horizon:identity:users:index')
@method_decorator(sensitive_post_parameters('password', @method_decorator(sensitive_post_parameters('password',
'confirm_password')) 'confirm_password'))
@@ -115,7 +131,7 @@ class CreateView(forms.ModalFormView):
try: try:
roles = api.keystone.role_list(self.request) roles = api.keystone.role_list(self.request)
except Exception: except Exception:
redirect = reverse("horizon:admin:users:index") redirect = reverse("horizon:identity:users:index")
exceptions.handle(self.request, exceptions.handle(self.request,
_("Unable to retrieve user roles."), _("Unable to retrieve user roles."),
redirect=redirect) 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 # same for user_id
if target.get('user_id') is None: if target.get('user_id') is None:
target['user_id'] = user.id 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) credentials = _user_to_credentials(request, user)

View File

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

View File

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