From d65ebe2054858f3630f6caf5c13b8bbcbe6ace1d Mon Sep 17 00:00:00 2001 From: shutingm Date: Wed, 21 Nov 2018 14:44:30 +0800 Subject: [PATCH] Add RBAC policies feature to horizon dashboard Add RBAC Policies panel to support Role-Based Access Control functionality. Implements: blueprint rbac-policies Change-Id: I883ad629d735dadf49e8bf9c50475050fdfcf797 --- doc/source/configuration/settings.rst | 11 ++ openstack_dashboard/api/neutron.py | 56 ++++++ .../admin/rbac_policies/__init__.py | 0 .../dashboards/admin/rbac_policies/forms.py | 170 ++++++++++++++++++ .../dashboards/admin/rbac_policies/panel.py | 44 +++++ .../dashboards/admin/rbac_policies/tables.py | 82 +++++++++ .../dashboards/admin/rbac_policies/tabs.py | 57 ++++++ .../templates/rbac_policies/_create.html | 7 + .../rbac_policies/_detail_overview.html | 18 ++ .../templates/rbac_policies/_update.html | 7 + .../templates/rbac_policies/create.html | 7 + .../templates/rbac_policies/detail.html | 20 +++ .../templates/rbac_policies/update.html | 7 + .../dashboards/admin/rbac_policies/tests.py | 122 +++++++++++++ .../dashboards/admin/rbac_policies/urls.py | 30 ++++ .../dashboards/admin/rbac_policies/views.py | 144 +++++++++++++++ .../_2350_admin_rbac_policies_panel.py | 10 ++ openstack_dashboard/test/settings.py | 5 + .../test/test_data/neutron_data.py | 22 +++ ...eutron-rbac-policies-9cv77nu2k93ieh4r.yaml | 11 ++ 20 files changed, 830 insertions(+) create mode 100644 openstack_dashboard/dashboards/admin/rbac_policies/__init__.py create mode 100644 openstack_dashboard/dashboards/admin/rbac_policies/forms.py create mode 100644 openstack_dashboard/dashboards/admin/rbac_policies/panel.py create mode 100644 openstack_dashboard/dashboards/admin/rbac_policies/tables.py create mode 100644 openstack_dashboard/dashboards/admin/rbac_policies/tabs.py create mode 100644 openstack_dashboard/dashboards/admin/rbac_policies/templates/rbac_policies/_create.html create mode 100644 openstack_dashboard/dashboards/admin/rbac_policies/templates/rbac_policies/_detail_overview.html create mode 100644 openstack_dashboard/dashboards/admin/rbac_policies/templates/rbac_policies/_update.html create mode 100644 openstack_dashboard/dashboards/admin/rbac_policies/templates/rbac_policies/create.html create mode 100644 openstack_dashboard/dashboards/admin/rbac_policies/templates/rbac_policies/detail.html create mode 100644 openstack_dashboard/dashboards/admin/rbac_policies/templates/rbac_policies/update.html create mode 100644 openstack_dashboard/dashboards/admin/rbac_policies/tests.py create mode 100644 openstack_dashboard/dashboards/admin/rbac_policies/urls.py create mode 100644 openstack_dashboard/dashboards/admin/rbac_policies/views.py create mode 100644 openstack_dashboard/enabled/_2350_admin_rbac_policies_panel.py create mode 100644 releasenotes/notes/bp-neutron-rbac-policies-9cv77nu2k93ieh4r.yaml diff --git a/doc/source/configuration/settings.rst b/doc/source/configuration/settings.rst index 8220bafdf0..68438c4b28 100644 --- a/doc/source/configuration/settings.rst +++ b/doc/source/configuration/settings.rst @@ -1645,6 +1645,7 @@ Default: 'enable_ha_router': False, 'enable_ipv6': True, 'enable_quotas': False, + 'enable_rbac_policy': True, 'enable_router': True, 'extra_provider_types': {}, 'physical_networks': [], @@ -1749,6 +1750,16 @@ Enable support for Neutron quotas feature. To make this feature work appropriately, you need to use Neutron plugins with quotas extension support and quota_driver should be DbQuotaDriver (default config). +enable_rbac_policy +################## + +.. versionadded:: 15.0.0(Stein) + +Default: ``True`` + +Set this to True to enable RBAC Policies panel that provide the ability for +users to use RBAC function. This option only affects when Neutron is enabled. + enable_router ############# diff --git a/openstack_dashboard/api/neutron.py b/openstack_dashboard/api/neutron.py index e62b12c4c8..31777835bf 100644 --- a/openstack_dashboard/api/neutron.py +++ b/openstack_dashboard/api/neutron.py @@ -1988,3 +1988,59 @@ def list_availability_zones(request, resource=None, state=None): az_list = [az for az in az_list if az['state'] == state] return sorted(az_list, key=lambda zone: zone['name']) + + +class RBACPolicy(NeutronAPIDictWrapper): + """Wrapper for neutron RBAC Policy.""" + + +def rbac_policy_create(request, **kwargs): + """Create a RBAC Policy. + + :param request: request context + :param target_tenant: target tenant of the policy + :param tenant_id: owner tenant of the policy(Not recommended) + :param object_type: network or qos_policy + :param object_id: object id of policy + :param action: access_as_shared or access_as_external + :return: RBACPolicy object + """ + body = {'rbac_policy': kwargs} + rbac_policy = neutronclient(request).create_rbac_policy( + body=body).get('rbac_policy') + return RBACPolicy(rbac_policy) + + +def rbac_policy_list(request, **kwargs): + """List of RBAC Policies.""" + policies = neutronclient(request).list_rbac_policies( + **kwargs).get('rbac_policies') + return [RBACPolicy(p) for p in policies] + + +def rbac_policy_update(request, policy_id, **kwargs): + """Update a RBAC Policy. + + :param request: request context + :param policy_id: target policy id + :param target_tenant: target tenant of the policy + :return: RBACPolicy object + """ + body = {'rbac_policy': kwargs} + rbac_policy = neutronclient(request).update_rbac_policy( + policy_id, body=body).get('rbac_policy') + return RBACPolicy(rbac_policy) + + +@profiler.trace +def rbac_policy_get(request, policy_id, **kwargs): + """Get RBAC policy for a given policy id.""" + policy = neutronclient(request).show_rbac_policy( + policy_id, **kwargs).get('rbac_policy') + return RBACPolicy(policy) + + +@profiler.trace +def rbac_policy_delete(request, policy_id): + """Delete RBAC policy for a given policy id.""" + neutronclient(request).delete_rbac_policy(policy_id) diff --git a/openstack_dashboard/dashboards/admin/rbac_policies/__init__.py b/openstack_dashboard/dashboards/admin/rbac_policies/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openstack_dashboard/dashboards/admin/rbac_policies/forms.py b/openstack_dashboard/dashboards/admin/rbac_policies/forms.py new file mode 100644 index 0000000000..767557a23f --- /dev/null +++ b/openstack_dashboard/dashboards/admin/rbac_policies/forms.py @@ -0,0 +1,170 @@ +# Copyright 2019 vmware, 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 logging + +from django.urls import reverse +from django.utils.translation import ugettext_lazy as _ + +from horizon import exceptions +from horizon import forms +from horizon import messages + +from openstack_dashboard import api + + +LOG = logging.getLogger(__name__) + + +# Predefined provider types. +ACTIONS = [ + { + 'name': 'access_as_shared', + 'value': _('Access as Shared') + }, + { + 'name': 'access_as_external', + 'value': _('Access as External') + } +] + +# Predefined provider object types. +OBJECT_TYPES = [ + { + 'name': 'network', + 'value': _('Network') + } +] + +QOS_POLICY_TYPE = { + 'name': 'qos_policy', + 'value': _('QoS Policy') +} + + +class CreatePolicyForm(forms.SelfHandlingForm): + target_tenant = forms.ThemableChoiceField(label=_("Target Project")) + object_type = forms.ThemableChoiceField( + label=_("Object Type"), + widget=forms.ThemableSelectWidget( + attrs={ + 'class': 'switchable', + 'data-slug': 'object_type' + })) + network_id = forms.ThemableChoiceField( + label=_("Network"), + widget=forms.ThemableSelectWidget(attrs={ + 'class': 'switched', + 'data-switch-on': 'object_type', + }), + required=False) + qos_policy_id = forms.ThemableChoiceField( + label=_("QoS Policy"), + widget=forms.ThemableSelectWidget(attrs={ + 'class': 'switched', + 'data-switch-on': 'object_type', + }), + required=False) + action = forms.ThemableChoiceField(label=_("Action")) + + def __init__(self, request, *args, **kwargs): + super(CreatePolicyForm, self).__init__(request, *args, **kwargs) + tenant_choices = [('', _("Select a project"))] + tenants, has_more = api.keystone.tenant_list(request) + tenant_choices.append(("*", "*")) + for tenant in tenants: + tenant_choices.append((tenant.id, tenant.name)) + self.fields['target_tenant'].choices = tenant_choices + action_choices = [('', _("Select an action"))] + for action in ACTIONS: + action_choices.append((action['name'], + action['value'])) + self.fields['action'].choices = action_choices + network_choices = [] + networks = api.neutron.network_list(request) + for network in networks: + network_choices.append((network.id, network.name)) + self.fields['network_id'].choices = network_choices + + # If enable QoS Policy + if api.neutron.is_extension_supported(request, extension_alias='qos'): + qos_policies = api.neutron.policy_list(request) + qos_choices = [(qos_policy['id'], qos_policy['name']) + for qos_policy in qos_policies] + self.fields['qos_policy_id'].choices = qos_choices + if QOS_POLICY_TYPE not in OBJECT_TYPES: + OBJECT_TYPES.append(QOS_POLICY_TYPE) + + object_type_choices = [('', _("Select an object type"))] + for object_type in OBJECT_TYPES: + object_type_choices.append((object_type['name'], + object_type['value'])) + self.fields['object_type'].choices = object_type_choices + + # Register object types which required + self.fields['network_id'].widget.attrs.update( + {'data-object_type-network': _('Network')}) + self.fields['qos_policy_id'].widget.attrs.update( + {'data-object_type-qos_policy': _('QoS Policy')}) + + def handle(self, request, data): + try: + params = { + 'target_tenant': data['target_tenant'], + 'action': data['action'], + 'object_type': data['object_type'], + } + if data['object_type'] == 'network': + params['object_id'] = data['network_id'] + elif data['object_type'] == 'qos_policy': + params['object_id'] = data['qos_policy_id'] + + rbac_policy = api.neutron.rbac_policy_create(request, **params) + msg = _('RBAC Policy was successfully created.') + messages.success(request, msg) + return rbac_policy + except Exception: + redirect = reverse('horizon:admin:rbac_policies:index') + msg = _('Failed to create a rbac policy.') + exceptions.handle(request, msg, redirect=redirect) + return False + + +class UpdatePolicyForm(forms.SelfHandlingForm): + target_tenant = forms.ThemableChoiceField(label=_("Target Project")) + failure_url = 'horizon:admin:rbac_policies:index' + + def __init__(self, request, *args, **kwargs): + super(UpdatePolicyForm, self).__init__(request, *args, **kwargs) + tenant_choices = [('', _("Select a project"))] + tenants, has_more = api.keystone.tenant_list(request) + for tenant in tenants: + tenant_choices.append((tenant.id, tenant.name)) + self.fields['target_tenant'].choices = tenant_choices + + def handle(self, request, data): + try: + params = {'target_tenant': data['target_tenant']} + rbac_policy = api.neutron.rbac_policy_update( + request, self.initial['rbac_policy_id'], **params) + msg = _('RBAC Policy %s was successfully updated.') \ + % self.initial['rbac_policy_id'] + messages.success(request, msg) + return rbac_policy + except Exception as e: + LOG.info('Failed to update rbac policy %(id)s: %(exc)s', + {'id': self.initial['rbac_policy_id'], 'exc': e}) + msg = _('Failed to update rbac policy %s') \ + % self.initial['rbac_policy_id'] + redirect = reverse(self.failure_url) + exceptions.handle(request, msg, redirect=redirect) diff --git a/openstack_dashboard/dashboards/admin/rbac_policies/panel.py b/openstack_dashboard/dashboards/admin/rbac_policies/panel.py new file mode 100644 index 0000000000..72e78f7611 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/rbac_policies/panel.py @@ -0,0 +1,44 @@ +# 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 logging + +from django.conf import settings +from django.utils.translation import ugettext_lazy as _ + +import horizon + +from openstack_dashboard.api import neutron + +LOG = logging.getLogger(__name__) + + +class RBACPolicies(horizon.Panel): + name = _("RBAC Policies") + slug = "rbac_policies" + permissions = ('openstack.services.network',) + policy_rules = (("network", "context_is_admin"),) + + def allowed(self, context): + request = context['request'] + network_config = getattr(settings, 'OPENSTACK_NEUTRON_NETWORK', {}) + try: + return ( + network_config.get('enable_rbac_policy', True) and + neutron.is_extension_supported(request, + extension_alias='rbac-policies') + ) + except Exception: + LOG.error("Call to list enabled services failed. This is likely " + "due to a problem communicating with the Neutron " + "endpoint. RBAC Policies panel will not be displayed.") + return False diff --git a/openstack_dashboard/dashboards/admin/rbac_policies/tables.py b/openstack_dashboard/dashboards/admin/rbac_policies/tables.py new file mode 100644 index 0000000000..5298b3e72b --- /dev/null +++ b/openstack_dashboard/dashboards/admin/rbac_policies/tables.py @@ -0,0 +1,82 @@ +# Copyright 2019 vmware, 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. + +from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import ungettext_lazy + +from horizon import tables + +from openstack_dashboard import api +from openstack_dashboard import policy + + +class CreateRBACPolicy(policy.PolicyTargetMixin, tables.LinkAction): + name = "create" + verbose_name = _("Create RBAC Policy") + url = "horizon:admin:rbac_policies:create" + classes = ("ajax-modal",) + icon = "plus" + policy_rules = (("network", "create_rbac_policy"),) + + +class DeleteRBACPolicy(policy.PolicyTargetMixin, tables.DeleteAction): + help_text = _("Deleted RBAC policy is not recoverable.") + + @staticmethod + def action_present(count): + return ungettext_lazy( + u"Delete RBAC Policy", + u"Delete RBAC Policies", + count + ) + + @staticmethod + def action_past(count): + return ungettext_lazy( + u"Deleted RBAC Policy", + u"Deleted RBAC Policies", + count + ) + + policy_rules = (("network", "delete_rbac_policy"),) + + def delete(self, request, obj_id): + api.neutron.rbac_policy_delete(request, obj_id) + + +class UpdateRBACPolicy(policy.PolicyTargetMixin, tables.LinkAction): + name = "update" + verbose_name = _("Edit Policy") + url = "horizon:admin:rbac_policies:update" + classes = ("ajax-modal",) + icon = "pencil" + policy_rules = (("network", "update_rbac_policy"),) + + +class RBACPoliciesTable(tables.DataTable): + tenant = tables.Column("tenant_name", verbose_name=_("Project")) + id = tables.WrappingColumn('id', + verbose_name=_('ID'), + link="horizon:admin:rbac_policies:detail") + object_type = tables.WrappingColumn('object_type', + verbose_name=_('Object Type')) + object_name = tables.Column("object_name", verbose_name=_("Object")) + target_tenant = tables.Column("target_tenant_name", + verbose_name=_("Target Project")) + + class Meta(object): + name = "rbac policies" + verbose_name = _("RBAC Policies") + table_actions = (CreateRBACPolicy, DeleteRBACPolicy,) + row_actions = (UpdateRBACPolicy, DeleteRBACPolicy) diff --git a/openstack_dashboard/dashboards/admin/rbac_policies/tabs.py b/openstack_dashboard/dashboards/admin/rbac_policies/tabs.py new file mode 100644 index 0000000000..80dd2bf9e0 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/rbac_policies/tabs.py @@ -0,0 +1,57 @@ +# Copyright 2019 vmware, Inc. +# All Rights Reserved. +# +# 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 _ + +from horizon import exceptions +from horizon import tabs +from horizon.utils import memoized + +from openstack_dashboard import api + + +class OverviewTab(tabs.Tab): + name = _("Overview") + slug = "overview" + template_name = "admin/rbac_policies/_detail_overview.html" + preload = False + + @memoized.memoized_method + def _get_data(self): + rbac_policy = {} + rbac_policy_id = None + try: + rbac_policy_id = self.tab_group.kwargs['rbac_policy_id'] + rbac_policy = api.neutron.rbac_policy_get(self.request, + rbac_policy_id) + + except Exception: + msg = _('Unable to retrieve details for rbac_policy "%s".') \ + % (rbac_policy_id) + exceptions.handle(self.request, msg) + return rbac_policy + + def get_context_data(self, request, **kwargs): + context = super(OverviewTab, self).get_context_data(request, **kwargs) + rbac_policy = self._get_data() + + context["rbac_policy"] = rbac_policy + return context + + +class RBACDetailsTabs(tabs.DetailTabsGroup): + slug = "rbac_policy_tabs" + tabs = (OverviewTab, ) + sticky = True diff --git a/openstack_dashboard/dashboards/admin/rbac_policies/templates/rbac_policies/_create.html b/openstack_dashboard/dashboards/admin/rbac_policies/templates/rbac_policies/_create.html new file mode 100644 index 0000000000..398194ce93 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/rbac_policies/templates/rbac_policies/_create.html @@ -0,0 +1,7 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} + +{% block modal-body-right %} +

{% trans "Description:" %}

+

{% trans "From here you can create a rbac policy." %}

+{% endblock %} diff --git a/openstack_dashboard/dashboards/admin/rbac_policies/templates/rbac_policies/_detail_overview.html b/openstack_dashboard/dashboards/admin/rbac_policies/templates/rbac_policies/_detail_overview.html new file mode 100644 index 0000000000..bdb633ba58 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/rbac_policies/templates/rbac_policies/_detail_overview.html @@ -0,0 +1,18 @@ +{% load i18n sizeformat %} + +
+
+
{% trans "ID" %}
+
{{ rbac_policy.id|default:_("None") }}
+
{% trans "Project ID" %}
+
{{ rbac_policy.project_id|default:_("-") }}
+
{% trans "Object Type" %}
+
{{ rbac_policy.object_type|default:_("Unknown") }}
+
{% trans "Object ID" %}
+
{{ rbac_policy.object_id|default:_("Unknown") }}
+
{% trans "Action" %}
+
{{ rbac_policy.action|default:_("Unknown") }}
+
{% trans "Target Tenant" %}
+
{{ rbac_policy.target_tenant|default:_("None") }}
+
+
diff --git a/openstack_dashboard/dashboards/admin/rbac_policies/templates/rbac_policies/_update.html b/openstack_dashboard/dashboards/admin/rbac_policies/templates/rbac_policies/_update.html new file mode 100644 index 0000000000..9f6906972b --- /dev/null +++ b/openstack_dashboard/dashboards/admin/rbac_policies/templates/rbac_policies/_update.html @@ -0,0 +1,7 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} + +{% block modal-body-right %} +

{% trans "Description:" %}

+

{% trans "You may update the editable properties of the RBAC policy here." %}

+{% endblock %} diff --git a/openstack_dashboard/dashboards/admin/rbac_policies/templates/rbac_policies/create.html b/openstack_dashboard/dashboards/admin/rbac_policies/templates/rbac_policies/create.html new file mode 100644 index 0000000000..3aee196656 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/rbac_policies/templates/rbac_policies/create.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Create a RBAC Policy" %}{% endblock %} + +{% block main %} + {% include 'admin/rbac_policies/_create.html' %} +{% endblock %} diff --git a/openstack_dashboard/dashboards/admin/rbac_policies/templates/rbac_policies/detail.html b/openstack_dashboard/dashboards/admin/rbac_policies/templates/rbac_policies/detail.html new file mode 100644 index 0000000000..bea1ced106 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/rbac_policies/templates/rbac_policies/detail.html @@ -0,0 +1,20 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "RBAC Policy Details" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_detail_header.html" %} +{% endblock %} + +{% block main %} +
+
+
+
+
+ {{ tab_group.render }} +
+
+
+
+{% endblock %} diff --git a/openstack_dashboard/dashboards/admin/rbac_policies/templates/rbac_policies/update.html b/openstack_dashboard/dashboards/admin/rbac_policies/templates/rbac_policies/update.html new file mode 100644 index 0000000000..a9fcb5dcb6 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/rbac_policies/templates/rbac_policies/update.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Update RBAC Policy" %}{% endblock %} + +{% block main %} + {% include 'project/rbac_policies/_update.html' %} +{% endblock %} diff --git a/openstack_dashboard/dashboards/admin/rbac_policies/tests.py b/openstack_dashboard/dashboards/admin/rbac_policies/tests.py new file mode 100644 index 0000000000..8e7e71a77c --- /dev/null +++ b/openstack_dashboard/dashboards/admin/rbac_policies/tests.py @@ -0,0 +1,122 @@ +# Copyright 2019 vmware, 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. +from django.urls import reverse + +from openstack_dashboard import api +from openstack_dashboard.test import helpers as test + +INDEX_TEMPLATE = 'horizon/common/_data_table_view.html' +INDEX_URL = reverse('horizon:admin:rbac_policies:index') + + +class RBACPolicyTests(test.BaseAdminViewTests): + + @test.create_mocks({api.neutron: ('rbac_policy_list', + 'network_list', + 'policy_list', + 'is_extension_supported',), + api.keystone: ('tenant_list',)}) + def test_index(self): + tenants = self.tenants.list() + + self.mock_tenant_list.return_value = [tenants, False] + self.mock_network_list.return_value = self.networks.list() + self.mock_policy_list.return_value = self.qos_policies.list() + self.mock_rbac_policy_list.return_value = self.rbac_policies.list() + self.mock_is_extension_supported.return_value = True + + res = self.client.get(INDEX_URL) + self.assertTemplateUsed(res, INDEX_TEMPLATE) + rbac_policies = res.context['table'].data + self.assertItemsEqual(rbac_policies, self.rbac_policies.list()) + self.mock_network_list.assert_called_once_with(test.IsHttpRequest()) + self.mock_policy_list.assert_called_once_with(test.IsHttpRequest()) + self.mock_tenant_list.assert_called_once_with(test.IsHttpRequest()) + self.mock_is_extension_supported.assert_called_once_with( + test.IsHttpRequest(), extension_alias='qos') + self.mock_rbac_policy_list.assert_called_once_with( + test.IsHttpRequest()) + + @test.create_mocks({api.neutron: ('network_list', + 'rbac_policy_create', + 'is_extension_supported',), + api.keystone: ('tenant_list',)}) + def test_rbac_create_post_with_network_type(self): + network = self.networks.first() + tenants = self.tenants.list() + rbac_policy = self.rbac_policies.first() + + self.mock_tenant_list.return_value = [tenants, False] + self.mock_network_list.return_value = self.networks.list() + self.mock_is_extension_supported.return_value = False + self.mock_rbac_policy_create.return_value = rbac_policy + + form_data = {'target_tenant': rbac_policy.target_tenant, + 'action': 'access_as_external', + 'object_type': 'network', + 'network_id': network.id} + url = reverse('horizon:admin:rbac_policies:create') + res = self.client.post(url, form_data) + + self.assertNoFormErrors(res) + self.assertRedirectsNoFollow(res, INDEX_URL) + + self.mock_tenant_list.assert_called_once_with(test.IsHttpRequest()) + self.mock_network_list.assert_called_once_with(test.IsHttpRequest()) + self.mock_is_extension_supported.assert_called_once_with( + test.IsHttpRequest(), extension_alias='qos') + params = {'target_tenant': rbac_policy.target_tenant, + 'action': 'access_as_external', + 'object_type': 'network', + 'object_id': network.id} + self.mock_rbac_policy_create.assert_called_once_with( + test.IsHttpRequest(), **params) + + @test.create_mocks({api.neutron: ('network_list', + 'policy_list', + 'rbac_policy_create', + 'is_extension_supported',), + api.keystone: ('tenant_list',)}) + def test_rbac_create_post_with_qos_policy_type(self): + qos_policy = self.qos_policies.first() + tenants = self.tenants.list() + rbac_policy = self.rbac_policies.filter(object_type="qos_policy")[0] + + self.mock_tenant_list.return_value = [tenants, False] + self.mock_network_list.return_value = self.networks.list() + self.mock_policy_list.return_value = self.qos_policies.list() + self.mock_is_extension_supported.return_value = True + self.mock_rbac_policy_create.return_value = rbac_policy + + form_data = {'target_tenant': rbac_policy.target_tenant, + 'action': 'access_as_shared', + 'object_type': 'qos_policy', + 'qos_policy_id': qos_policy.id} + url = reverse('horizon:admin:rbac_policies:create') + res = self.client.post(url, form_data) + + self.assertNoFormErrors(res) + self.assertRedirectsNoFollow(res, INDEX_URL) + + self.mock_tenant_list.assert_called_once_with(test.IsHttpRequest()) + self.mock_network_list.assert_called_once_with(test.IsHttpRequest()) + self.mock_policy_list.assert_called_once_with(test.IsHttpRequest()) + self.mock_is_extension_supported.assert_called_once_with( + test.IsHttpRequest(), extension_alias='qos') + params = {'target_tenant': rbac_policy.target_tenant, + 'action': 'access_as_shared', + 'object_type': 'qos_policy', + 'object_id': qos_policy.id} + self.mock_rbac_policy_create.assert_called_once_with( + test.IsHttpRequest(), **params) diff --git a/openstack_dashboard/dashboards/admin/rbac_policies/urls.py b/openstack_dashboard/dashboards/admin/rbac_policies/urls.py new file mode 100644 index 0000000000..b7a1ab8588 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/rbac_policies/urls.py @@ -0,0 +1,30 @@ +# 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.conf.urls import url + +from openstack_dashboard.dashboards.admin.rbac_policies import views + + +RBAC_POLICY_URL = r'^(?P[^/]+)/%s$' + + +urlpatterns = [ + url(r'^$', views.IndexView.as_view(), name='index'), + url(r'^create/$', views.CreateView.as_view(), name='create'), + url(RBAC_POLICY_URL % '$', + views.DetailView.as_view(), + name='detail'), + url(RBAC_POLICY_URL % 'update', + views.UpdateView.as_view(), + name='update'), +] diff --git a/openstack_dashboard/dashboards/admin/rbac_policies/views.py b/openstack_dashboard/dashboards/admin/rbac_policies/views.py new file mode 100644 index 0000000000..b517aa8ecc --- /dev/null +++ b/openstack_dashboard/dashboards/admin/rbac_policies/views.py @@ -0,0 +1,144 @@ +# 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 collections import OrderedDict + +from django.urls import reverse +from django.urls import reverse_lazy +from django.utils.translation import ugettext_lazy as _ + +from horizon import exceptions +from horizon import forms +from horizon import messages +from horizon import tables +from horizon import tabs +from horizon.utils import memoized + +from openstack_dashboard import api +from openstack_dashboard.dashboards.admin.rbac_policies \ + import forms as rbac_policy_forms +from openstack_dashboard.dashboards.admin.rbac_policies \ + import tables as rbac_policy_tables +from openstack_dashboard.dashboards.admin.rbac_policies \ + import tabs as rbac_policy_tabs + + +class IndexView(tables.DataTableView): + table_class = rbac_policy_tables.RBACPoliciesTable + page_title = _("RBAC Policies") + + @memoized.memoized_method + def _get_tenants(self): + try: + tenants, has_more = api.keystone.tenant_list(self.request) + except Exception: + tenants = [] + msg = _("Unable to retrieve information about the " + "policies' projects.") + exceptions.handle(self.request, msg) + + tenant_dict = OrderedDict([(t.id, t.name) for t in tenants]) + return tenant_dict + + def _get_networks(self): + try: + networks = api.neutron.network_list(self.request) + except Exception: + networks = [] + msg = _("Unable to retrieve information about the " + "policies' networks.") + exceptions.handle(self.request, msg) + return dict([(n.id, n.name) for n in networks]) + + def _get_qos_policies(self): + qos_policies = [] + try: + if api.neutron.is_extension_supported(self.request, + extension_alias='qos'): + qos_policies = api.neutron.policy_list(self.request) + except Exception: + msg = _("Unable to retrieve information about the " + "policies' qos policies.") + exceptions.handle(self.request, msg) + return dict([(q.id, q.name) for q in qos_policies]) + + def get_data(self): + try: + rbac_policies = api.neutron.rbac_policy_list(self.request) + except Exception: + rbac_policies = [] + messages.error(self.request, + _("Unable to retrieve RBAC policies.")) + if rbac_policies: + tenant_dict = self._get_tenants() + network_dict = self._get_networks() + qos_policy_dict = self._get_qos_policies() + for p in rbac_policies: + # Set tenant name and object name + p.tenant_name = tenant_dict.get(p.tenant_id, p.tenant_id) + p.target_tenant_name = tenant_dict.get(p.target_tenant, + p.target_tenant) + if p.object_type == "network": + p.object_name = network_dict.get(p.object_id, p.object_id) + elif p.object_type == "qos_policy": + p.object_name = qos_policy_dict.get(p.object_id, + p.object_id) + return rbac_policies + + +class CreateView(forms.ModalFormView): + template_name = 'admin/rbac_policies/create.html' + form_id = "create_rbac_policy_form" + form_class = rbac_policy_forms.CreatePolicyForm + submit_label = _("Create RBAC Policy") + submit_url = reverse_lazy("horizon:admin:rbac_policies:create") + success_url = reverse_lazy("horizon:admin:rbac_policies:index") + page_title = _("Create A RBAC Policy") + + +class UpdateView(forms.ModalFormView): + context_object_name = 'rbac_policies' + template_name = 'admin/rbac_policies/update.html' + form_class = rbac_policy_forms.UpdatePolicyForm + form_id = "update_rbac_policy_form" + submit_label = _("Save Changes") + submit_url = 'horizon:admin:rbac_policies:update' + success_url = reverse_lazy('horizon:admin:rbac_policies:index') + page_title = _("Update RBAC Policy") + + def get_context_data(self, **kwargs): + context = super(UpdateView, self).get_context_data(**kwargs) + args = (self.kwargs['rbac_policy_id'],) + context["rbac_policy_id"] = self.kwargs['rbac_policy_id'] + context["submit_url"] = reverse(self.submit_url, args=args) + return context + + @memoized.memoized_method + def _get_object(self, *args, **kwargs): + rbac_policy_id = self.kwargs['rbac_policy_id'] + try: + return api.neutron.rbac_policy_get(self.request, rbac_policy_id) + except Exception: + redirect = self.success_url + msg = _('Unable to retrieve rbac policy details.') + exceptions.handle(self.request, msg, redirect=redirect) + + def get_initial(self): + rbac_policy = self._get_object() + return {'rbac_policy_id': rbac_policy['id'], + 'target_tenant': rbac_policy['target_tenant']} + + +class DetailView(tabs.TabView): + tab_group_class = rbac_policy_tabs.RBACDetailsTabs + template_name = 'horizon/common/_detail.html' + page_title = "{{ rbac_policy.id }}" diff --git a/openstack_dashboard/enabled/_2350_admin_rbac_policies_panel.py b/openstack_dashboard/enabled/_2350_admin_rbac_policies_panel.py new file mode 100644 index 0000000000..c566f37424 --- /dev/null +++ b/openstack_dashboard/enabled/_2350_admin_rbac_policies_panel.py @@ -0,0 +1,10 @@ +# The slug of the panel to be added to HORIZON_CONFIG. Required. +PANEL = 'rbac_policies' +# The slug of the dashboard the PANEL associated with. Required. +PANEL_DASHBOARD = 'admin' +# The slug of the panel group the PANEL is associated with. +PANEL_GROUP = 'network' + +# Python panel class of the PANEL to be added. +ADD_PANEL = ('openstack_dashboard.dashboards.admin.' + 'rbac_policies.panel.RBACPolicies') diff --git a/openstack_dashboard/test/settings.py b/openstack_dashboard/test/settings.py index 7e149124ce..6ec7c4b120 100644 --- a/openstack_dashboard/test/settings.py +++ b/openstack_dashboard/test/settings.py @@ -300,6 +300,11 @@ TEST_GLOBAL_MOCKS_ON_PANELS = { '.network_qos.panel.NetworkQoS.can_access'), 'return_value': True, }, + 'rbac_policies': { + 'method': ('openstack_dashboard.dashboards.admin' + '.rbac_policies.panel.RBACPolicies.can_access'), + 'return_value': True, + }, 'server_groups': { 'method': ('openstack_dashboard.dashboards.project' '.server_groups.panel.ServerGroups.can_access'), diff --git a/openstack_dashboard/test/test_data/neutron_data.py b/openstack_dashboard/test/test_data/neutron_data.py index ffdf686b7e..cc43b39617 100644 --- a/openstack_dashboard/test/test_data/neutron_data.py +++ b/openstack_dashboard/test/test_data/neutron_data.py @@ -45,6 +45,7 @@ def data(TEST): TEST.neutron_quota_usages = utils.TestDataContainer() TEST.ip_availability = utils.TestDataContainer() TEST.qos_policies = utils.TestDataContainer() + TEST.rbac_policies = utils.TestDataContainer() TEST.tp_ports = utils.TestDataContainer() TEST.neutron_availability_zones = utils.TestDataContainer() @@ -66,6 +67,7 @@ def data(TEST): TEST.api_monitors = utils.TestDataContainer() TEST.api_extensions = utils.TestDataContainer() TEST.api_ip_availability = utils.TestDataContainer() + TEST.api_rbac_policies = utils.TestDataContainer() TEST.api_qos_policies = utils.TestDataContainer() TEST.api_tp_trunks = utils.TestDataContainer() TEST.api_tp_ports = utils.TestDataContainer() @@ -773,6 +775,26 @@ def data(TEST): TEST.api_qos_policies.add(policy_dict1) TEST.qos_policies.add(neutron.QoSPolicy(policy_dict1)) + # rbac policies + rbac_policy_dict = {"project_id": "1", + "object_type": "network", + "id": "7f27e61a-9863-448a-a769-eb922fdef3f8", + "object_id": "82288d84-e0a5-42ac-95be-e6af08727e42", + "target_tenant": "2", + "action": "access_as_external", + "tenant_id": "1"} + TEST.api_rbac_policies.add(rbac_policy_dict) + TEST.rbac_policies.add(neutron.RBACPolicy(rbac_policy_dict)) + rbac_policy_dict1 = {"project_id": "1", + "object_type": "qos_policy", + "id": "7f27e61a-9863-448a-a769-eb922fdef3f8", + "object_id": "a21dcd22-7189-cccc-aa32-22adafaf16a7", + "target_tenant": "2", + "action": "access_as_shared", + "tenant_id": "1"} + TEST.api_rbac_policies.add(rbac_policy_dict1) + TEST.rbac_policies.add(neutron.RBACPolicy(rbac_policy_dict1)) + # TRUNKPORT # # The test setup was created by the following command sequence: diff --git a/releasenotes/notes/bp-neutron-rbac-policies-9cv77nu2k93ieh4r.yaml b/releasenotes/notes/bp-neutron-rbac-policies-9cv77nu2k93ieh4r.yaml new file mode 100644 index 0000000000..29cfeafc0b --- /dev/null +++ b/releasenotes/notes/bp-neutron-rbac-policies-9cv77nu2k93ieh4r.yaml @@ -0,0 +1,11 @@ +--- +features: + - > + [`blueprint neutron-rbac-policies `_] + This blueprint adds RBAC policies panel to the Admin Network group. + This panel will be enabled by default when the RBAC extension is + enabled. Remove this panel by setting "'enable_rbac_policy': False" + in 'local_settings.py'. RBAC policy supports the control of two + resources: networks and qos policies, because qos policies is + an extension function of neutron, need to enable this extension + if wants to use it.