Files
horizon/openstack_dashboard/dashboards/project/security_groups/tables.py
Hang Yang c7ea66bc3e Support RBAC security groups in dashboard
Get the RBAC shared security groups in the dashboard by making
an additional Neutron API call to filter by the shared field. Currently,
the dashboard only shows SGs owned by the tenant.

Depends-On: https://review.opendev.org/c/openstack/neutron/+/811242
Closes-Bug: #1907843
Change-Id: Ifa1acb3f0f6a33d0b4dc3761674e561a8d24c5c2
2021-10-18 15:27:35 -05:00

294 lines
10 KiB
Python

# Copyright 2012 Nebula, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import functools
from django.conf import settings
from django.template import defaultfilters
from django.urls import reverse
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ungettext_lazy
from horizon import exceptions
from horizon import tables
from openstack_dashboard import api
from openstack_dashboard import policy
from openstack_dashboard.usage import quotas
from openstack_dashboard.utils import filters
class DeleteGroup(policy.PolicyTargetMixin, tables.DeleteAction):
policy_rules = (("network", "delete_security_group"),)
@staticmethod
def action_present(count):
return ungettext_lazy(
"Delete Security Group",
"Delete Security Groups",
count
)
@staticmethod
def action_past(count):
return ungettext_lazy(
"Deleted Security Group",
"Deleted Security Groups",
count
)
def allowed(self, request, security_group=None):
if not security_group:
return True
return security_group.name != 'default'
def delete(self, request, obj_id):
api.neutron.security_group_delete(request, obj_id)
class CreateGroup(policy.PolicyTargetMixin, tables.LinkAction):
name = "create"
verbose_name = _("Create Security Group")
url = "horizon:project:security_groups:create"
classes = ("ajax-modal",)
icon = "plus"
policy_rules = (("network", "create_security_group"),)
def allowed(self, request, security_group=None):
usages = quotas.tenant_quota_usages(request,
targets=('security_group', ))
if usages['security_group'].get('available', 1) <= 0:
if "disabled" not in self.classes:
self.classes = list(self.classes) + ['disabled']
self.verbose_name = _("Create Security Group (Quota exceeded)")
else:
self.verbose_name = _("Create Security Group")
self.classes = [c for c in self.classes if c != "disabled"]
return True
class EditGroup(policy.PolicyTargetMixin, tables.LinkAction):
name = "edit"
verbose_name = _("Edit Security Group")
url = "horizon:project:security_groups:update"
classes = ("ajax-modal",)
icon = "pencil"
policy_rules = (("network", "update_security_group"),)
def allowed(self, request, security_group=None):
if not security_group:
return True
return security_group.name != 'default'
class ManageRules(policy.PolicyTargetMixin, tables.LinkAction):
name = "manage_rules"
verbose_name = _("Manage Rules")
url = "horizon:project:security_groups:detail"
icon = "pencil"
policy_rules = (("network", "get_security_group_rule"),)
class SecurityGroupsFilterAction(tables.FilterAction):
def filter(self, table, security_groups, filter_string):
"""Naive case-insensitive search."""
query = filter_string.lower()
return [security_group for security_group in security_groups
if query in security_group.name.lower()]
class SecurityGroupsTable(tables.DataTable):
name = tables.Column("name", verbose_name=_("Name"))
security_group_id = tables.Column("id",
verbose_name=_("Security Group ID"))
description = tables.Column("description", verbose_name=_("Description"))
shared = tables.Column("shared", verbose_name=_("Shared"))
def __init__(self, request, *args, **kwargs):
super().__init__(request, *args, **kwargs)
try:
is_shared_supported = api.neutron.is_extension_supported(
self.request, 'security-groups-shared-filtering')
except Exception:
exceptions.handle(
self.request,
_('Failed to check if shared field is supported.'))
is_shared_supported = False
if not is_shared_supported:
del self.columns['shared']
def sanitize_id(self, obj_id):
return filters.get_int_or_uuid(obj_id)
class Meta(object):
name = "security_groups"
verbose_name = _("Security Groups")
table_actions = (CreateGroup, DeleteGroup, SecurityGroupsFilterAction)
row_actions = (ManageRules, EditGroup, DeleteGroup)
class CreateRule(policy.PolicyTargetMixin, tables.LinkAction):
name = "add_rule"
verbose_name = _("Add Rule")
url = "horizon:project:security_groups:add_rule"
classes = ("ajax-modal",)
icon = "plus"
policy_rules = (("network", "create_security_group_rule"),)
def get_link_url(self):
return reverse(self.url, args=[self.table.kwargs['security_group_id']])
def allowed(self, request, security_group=None):
usages = quotas.tenant_quota_usages(request,
targets=('security_group_rule', ))
self.classes = [c for c in self.classes if c != "disabled"]
if usages['security_group_rule'].get('available', 1) <= 0:
self.classes.append("disabled")
self.verbose_name = _("Add Rule (Quota exceeded)")
else:
self.verbose_name = _("Add Rule")
return True
class DeleteRule(policy.PolicyTargetMixin, tables.DeleteAction):
policy_rules = (("network", "delete_security_group_rule"),)
@staticmethod
def action_present(count):
return ungettext_lazy(
"Delete Rule",
"Delete Rules",
count
)
@staticmethod
def action_past(count):
return ungettext_lazy(
"Deleted Rule",
"Deleted Rules",
count
)
def delete(self, request, obj_id):
api.neutron.security_group_rule_delete(request, obj_id)
def get_success_url(self, request):
sg_id = self.table.kwargs['security_group_id']
return reverse("horizon:project:security_groups:detail", args=[sg_id])
def get_remote_ip_prefix(rule):
if 'cidr' in rule.ip_range:
if rule.ip_range['cidr'] is None:
range = '::/0' if rule.ethertype == 'IPv6' else '0.0.0.0/0'
else:
range = rule.ip_range['cidr']
return range
return None
def get_remote_security_group(rule):
return rule.group.get('name')
def get_port_range(rule):
# There is no case where from_port is None and to_port has a value,
# so it is enough to check only from_port.
if rule.from_port is None:
return _('Any')
ip_proto = rule.ip_protocol
if rule.from_port == rule.to_port:
return check_rule_template(rule.from_port, ip_proto)
return ("%(from)s - %(to)s" %
{'from': check_rule_template(rule.from_port, ip_proto),
'to': check_rule_template(rule.to_port, ip_proto)})
def filter_direction(direction):
if direction is None or direction.lower() == 'ingress':
return _('Ingress')
return _('Egress')
def filter_protocol(protocol):
if protocol is None:
return _('Any')
return protocol.upper()
def check_rule_template(port, ip_proto):
rules_dict = settings.SECURITY_GROUP_RULES
if not rules_dict:
return port
templ_rule = [rule for rule in rules_dict.values()
if (str(port) == rule['from_port'] and
str(port) == rule['to_port'] and
ip_proto == rule['ip_protocol'])]
if templ_rule:
return "%(from_port)s (%(name)s)" % templ_rule[0]
return port
class RulesTable(tables.DataTable):
direction = tables.Column("direction",
verbose_name=_("Direction"),
filters=(filter_direction,))
ethertype = tables.Column("ethertype",
verbose_name=_("Ether Type"))
protocol = tables.Column("ip_protocol",
verbose_name=_("IP Protocol"),
filters=(filter_protocol,))
port_range = tables.Column(get_port_range,
verbose_name=_("Port Range"))
remote_ip_prefix = tables.Column(get_remote_ip_prefix,
verbose_name=_("Remote IP Prefix"))
remote_security_group = tables.Column(get_remote_security_group,
verbose_name=_("Remote Security"
" Group"))
description = tables.Column(
"description",
verbose_name=("Description"),
# 'default' filter is to hide the difference between empty string
# and None (null) in description. Both will be displayed as '-'.
filters=(functools.partial(defaultfilters.default, arg=_("-")),))
def __init__(self, request, *args, **kwargs):
super().__init__(request, *args, **kwargs)
try:
is_desc_supported = api.neutron.is_extension_supported(
self.request, 'standard-attr-description')
except Exception:
exceptions.handle(
self.request,
_('Failed to check if description field is supported.'))
is_desc_supported = False
if not is_desc_supported:
del self.columns['description']
def sanitize_id(self, obj_id):
return filters.get_int_or_uuid(obj_id)
def get_object_display(self, rule):
return str(rule)
class Meta(object):
name = "rules"
verbose_name = _("Security Group Rules")
table_actions = (CreateRule, DeleteRule)
row_actions = (DeleteRule,)