Files
horizon/openstack_dashboard/dashboards/identity/projects/tables.py
Steve McLellan 018e99d20e Allow horizon to function without nova
Adds conditional block to nova quotas to exclude them if nova is not
enabled; adds 'permission' checks to the project overview and
access_and_security panels to only enable them if compute is enabled;
adds permission checks on compute and image to the admin overview
and metadef panels; disables 'modify quota' and 'view usage' project
actions; disables 'update defaults' if there are no quotas available.

The 'access and security' panel still appears (under Compute) but
tabs other than the keystone endpoint and RC download tab are hidden.

Closes-Bug: #1580116
Change-Id: I1b2ddee0395ad9f55692111604b31618c4eaf69e
2016-07-26 09:15:48 -05:00

317 lines
12 KiB
Python

# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django.core.exceptions import ValidationError # noqa
from django.core.urlresolvers import reverse
from django.template import defaultfilters as filters
from django.utils.http import urlencode
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ungettext_lazy
from horizon import exceptions
from horizon import forms
from horizon import tables
from keystoneclient.exceptions import Conflict # noqa
from openstack_dashboard import api
from openstack_dashboard import policy
from openstack_dashboard.usage import quotas
class RescopeTokenToProject(tables.LinkAction):
name = "rescope"
verbose_name = _("Set as Active Project")
url = "switch_tenants"
def allowed(self, request, project):
# allow rescoping token to any project the user has a role on,
# authorized_tenants, and that they are not currently scoped to
return next((True for proj in request.user.authorized_tenants
if proj.id == project.id and
project.id != request.user.project_id and
project.enabled), False)
def get_link_url(self, project):
# redirects to the switch_tenants url which then will redirect
# back to this page
dash_url = reverse("horizon:identity:projects:index")
base_url = reverse(self.url, args=[project.id])
param = urlencode({"next": dash_url})
return "?".join([base_url, param])
class UpdateMembersLink(tables.LinkAction):
name = "users"
verbose_name = _("Manage Members")
url = "horizon:identity:projects:update"
classes = ("ajax-modal",)
icon = "pencil"
policy_rules = (("identity", "identity:list_users"),
("identity", "identity:list_roles"))
def get_link_url(self, project):
step = 'update_members'
base_url = reverse(self.url, args=[project.id])
param = urlencode({"step": step})
return "?".join([base_url, param])
def allowed(self, request, project):
if api.keystone.is_multi_domain_enabled():
# domain admin or cloud admin = True
# project admin or member = False
return api.keystone.is_domain_admin(request)
else:
return super(UpdateMembersLink, self).allowed(request, project)
class UpdateGroupsLink(tables.LinkAction):
name = "groups"
verbose_name = _("Modify Groups")
url = "horizon:identity:projects:update"
classes = ("ajax-modal",)
icon = "pencil"
policy_rules = (("identity", "identity:list_groups"),)
def allowed(self, request, project):
if api.keystone.is_multi_domain_enabled():
# domain admin or cloud admin = True
# project admin or member = False
return api.keystone.is_domain_admin(request)
else:
return super(UpdateGroupsLink, self).allowed(request, project)
def get_link_url(self, project):
step = 'update_group_members'
base_url = reverse(self.url, args=[project.id])
param = urlencode({"step": step})
return "?".join([base_url, param])
class UsageLink(tables.LinkAction):
name = "usage"
verbose_name = _("View 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 and
api.base.is_service_enabled(request, 'compute'))
class CreateProject(tables.LinkAction):
name = "create"
verbose_name = _("Create Project")
url = "horizon:identity:projects:create"
classes = ("ajax-modal",)
icon = "plus"
policy_rules = (('identity', 'identity:create_project'),)
def allowed(self, request, project):
if api.keystone.is_multi_domain_enabled():
# domain admin or cloud admin = True
# project admin or member = False
return api.keystone.is_domain_admin(request)
else:
return api.keystone.keystone_can_edit_project()
class UpdateProject(policy.PolicyTargetMixin, tables.LinkAction):
name = "update"
verbose_name = _("Edit Project")
url = "horizon:identity:projects:update"
classes = ("ajax-modal",)
icon = "pencil"
policy_rules = (('identity', 'identity:update_project'),)
policy_target_attrs = (("target.project.domain_id", "domain_id"),)
def allowed(self, request, project):
if api.keystone.is_multi_domain_enabled():
# domain admin or cloud admin = True
# project admin or member = False
return api.keystone.is_domain_admin(request)
else:
return api.keystone.keystone_can_edit_project()
class ModifyQuotas(tables.LinkAction):
name = "quotas"
verbose_name = _("Modify Quotas")
url = "horizon:identity:projects:update"
classes = ("ajax-modal",)
icon = "pencil"
policy_rules = (('compute', "compute_extension:quotas:update"),)
def allowed(self, request, datum):
if api.keystone.VERSIONS.active < 3:
return True
else:
return (api.keystone.is_cloud_admin(request) and
quotas.enabled_quotas(request))
def get_link_url(self, project):
step = 'update_quotas'
base_url = reverse(self.url, args=[project.id])
param = urlencode({"step": step})
return "?".join([base_url, param])
class DeleteTenantsAction(policy.PolicyTargetMixin, tables.DeleteAction):
@staticmethod
def action_present(count):
return ungettext_lazy(
u"Delete Project",
u"Delete Projects",
count
)
@staticmethod
def action_past(count):
return ungettext_lazy(
u"Deleted Project",
u"Deleted Projects",
count
)
policy_rules = (("identity", "identity:delete_project"),)
policy_target_attrs = ("target.project.domain_id", "domain_id"),
def allowed(self, request, project):
if api.keystone.is_multi_domain_enabled() \
and not api.keystone.is_domain_admin(request):
return False
return api.keystone.keystone_can_edit_project()
def delete(self, request, obj_id):
api.keystone.tenant_delete(request, obj_id)
def handle(self, table, request, obj_ids):
response = \
super(DeleteTenantsAction, self).handle(table, request, obj_ids)
return response
class TenantFilterAction(tables.FilterAction):
def filter(self, table, tenants, filter_string):
"""Really naive case-insensitive search."""
# FIXME(gabriel): This should be smarter. Written for demo purposes.
q = filter_string.lower()
def comp(tenant):
if q in tenant.name.lower():
return True
return False
return filter(comp, tenants)
class UpdateRow(tables.Row):
ajax = True
def get_data(self, request, project_id):
project_info = api.keystone.tenant_get(request, project_id,
admin=True)
return project_info
class UpdateCell(tables.UpdateAction):
def allowed(self, request, project, cell):
policy_rule = (("identity", "identity:update_project"),)
return (
(cell.column.name != 'enabled' or
request.user.project_id != cell.datum.id) and
api.keystone.keystone_can_edit_project() and
policy.check(policy_rule, request))
def update_cell(self, request, datum, project_id,
cell_name, new_cell_value):
# inline update project info
try:
project_obj = datum
# updating changed value by new value
setattr(project_obj, cell_name, new_cell_value)
api.keystone.tenant_update(
request,
project_id,
name=project_obj.name,
description=project_obj.description,
enabled=project_obj.enabled)
except Conflict:
# Returning a nice error message about name conflict. The message
# from exception is not that clear for the users.
message = _("This name is already taken.")
raise ValidationError(message)
except Exception:
exceptions.handle(request, ignore=True)
return False
return True
class TenantsTable(tables.DataTable):
name = tables.Column('name', verbose_name=_('Name'),
link=("horizon:identity:projects:detail"),
form_field=forms.CharField(max_length=64),
update_action=UpdateCell)
description = tables.Column(lambda obj: getattr(obj, 'description', None),
verbose_name=_('Description'),
form_field=forms.CharField(
widget=forms.Textarea(attrs={'rows': 4}),
required=False),
update_action=UpdateCell)
id = tables.Column('id', verbose_name=_('Project ID'))
enabled = tables.Column('enabled', verbose_name=_('Enabled'), status=True,
filters=(filters.yesno, filters.capfirst),
form_field=forms.BooleanField(
label=_('Enabled'),
required=False),
update_action=UpdateCell)
if api.keystone.VERSIONS.active >= 3:
domain_name = tables.Column(
'domain_name', verbose_name=_('Domain Name'))
enabled = tables.Column('enabled', verbose_name=_('Enabled'),
status=True,
filters=(filters.yesno, filters.capfirst),
form_field=forms.BooleanField(
label=_('Enabled'),
required=False),
update_action=UpdateCell)
def get_project_detail_link(self, project):
# this method is an ugly monkey patch, needed because
# the column link method does not provide access to the request
if policy.check((("identity", "identity:get_project"),),
self.request, target={"project": project}):
return reverse("horizon:identity:projects:detail",
args=(project.id,))
return None
def __init__(self, request, data=None, needs_form_wrapper=None, **kwargs):
super(TenantsTable,
self).__init__(request, data=data,
needs_form_wrapper=needs_form_wrapper,
**kwargs)
# see the comment above about ugly monkey patches
self.columns['name'].get_link_url = self.get_project_detail_link
class Meta(object):
name = "tenants"
verbose_name = _("Projects")
row_class = UpdateRow
row_actions = (UpdateMembersLink, UpdateGroupsLink, UpdateProject,
UsageLink, ModifyQuotas, DeleteTenantsAction,
RescopeTokenToProject)
table_actions = (TenantFilterAction, CreateProject,
DeleteTenantsAction)
pagination_param = "tenant_marker"