Firewall / NAT: support category filters (https://github.com/opnsense/core/issues/4587)

add basic category grid, including menu registration and acl.
This commit is contained in:
Ad Schellevis 2021-01-18 18:43:32 +01:00
parent 1a646e087d
commit 349009df3a
7 changed files with 211 additions and 19 deletions

View File

@ -51,7 +51,7 @@ class CategoryController extends ApiMutableModelControllerBase
*/
public function searchItemAction()
{
return $this->searchBase("categories.category", array('name'), "name");
return $this->searchBase("categories.category", array('name', 'auto'), "name");
}
/**
@ -63,7 +63,15 @@ class CategoryController extends ApiMutableModelControllerBase
*/
public function setItemAction($uuid)
{
// XXX: handle renames?
$node = $this->getModel()->getNodeByReference('categories.category.' . $uuid);
$old_name = $node != null ? (string)$node->name : null;
if ($old_name !== null && $this->request->isPost() && $this->request->hasPost("category")) {
$new_name = $this->request->getPost("category")['name'];
if ($new_name != $old_name) {
// replace categories, setBase() will synchronise the changes to disk
$this->getModel()->refactor($old_name, $new_name);
}
}
return $this->setBase("category", "categories.category", $uuid);
}
@ -101,7 +109,12 @@ class CategoryController extends ApiMutableModelControllerBase
public function delItemAction($uuid)
{
Config::getInstance()->lock();
// XXX : check if used and throw error
$node = $this->getModel()->getNodeByReference('categories.category.' . $uuid);
$node_name = $node != null ? (string)$node->name : null;
if ($node_name != null && $this->getModel()->isUsed($node_name)) {
$message = gettext("Cannot delete a category which is still in use.");
throw new \OPNsense\Base\UserException($message, gettext("Category in use"));
}
return $this->delBase("categories.category", $uuid);
}
}

View File

@ -0,0 +1,43 @@
<?php
/*
* Copyright (C) 2020 Deciso B.V.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
* OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
namespace OPNsense\Firewall;
use OPNsense\Base\IndexController;
/**
* @package OPNsense\Firewall
*/
class CategoryController extends IndexController
{
public function indexAction($selected = null)
{
$this->view->formDialogEdit = $this->getForm("categoryEdit");
$this->view->pick('OPNsense/Firewall/category');
}
}

View File

@ -0,0 +1,14 @@
<form>
<field>
<id>category.auto</id>
<label>automatic</label>
<type>checkbox</type>
<help>Automatically added, will be removed when used</help>
</field>
<field>
<id>category.name</id>
<label>name</label>
<type>text</type>
<help>Enter a name for this category.</help>
</field>
</form>

View File

@ -211,6 +211,13 @@
<pattern>api/firewall/alias/get*</pattern>
</patterns>
</page-firewall-aliases>
<page-firewall-categories>
<name>Firewall: Categories</name>
<patterns>
<pattern>ui/firewall/category/*</pattern>
<pattern>api/firewall/category/*</pattern>
</patterns>
</page-firewall-categories>
<page-firewall-nat-1-1>
<name>Firewall: NAT: 1:1</name>
<patterns>

View File

@ -154,6 +154,7 @@
<Aliases url="/ui/firewall/alias" cssClass="fa fa-list-alt fa-fw">
<Edit url="/ui/firewall/alias/*" visibility="hidden"/>
</Aliases>
<Categories url="/ui/firewall/category" cssClass="fa fa-list-alt fa-fw"/>
<Groups url="/interfaces_groups.php" cssClass="fa fa-sitemap fa-fw">
<Edit url="/interfaces_groups_edit.php*" visibility="hidden"/>
</Groups>

View File

@ -57,15 +57,8 @@ class Category extends BaseModel
}
}
/**
* collect unique categories from rules and updates the model with auto generated items.
* XXX: if this operation turns out to be a bottleneck, we should move the maintance responsibiliy to the caller
* for the item in question (rule update)
* @return bool true if changed
*/
public function sync()
private function ruleIterator()
{
$has_changed = false;
$cfgObj = Config::getInstance()->object();
$source = [
['filter', 'rule'],
@ -73,8 +66,7 @@ class Category extends BaseModel
['nat', 'onetoone'],
['nat', 'outbound', 'rule'],
['nat', 'npt'],
];
$used_categories = [];
];
foreach ($source as $aliasref) {
$cfgsection = $cfgObj;
foreach ($aliasref as $cfgName) {
@ -85,15 +77,62 @@ class Category extends BaseModel
if ($cfgsection != null) {
foreach ($cfgsection as $node) {
if (!empty($node->category)) {
foreach (explode(",", (string)$node->category) as $cat) {
if (!in_array($cat, $used_categories)) {
$used_categories[] = $cat;
}
}
yield $node;
}
}
}
}
}
/**
* refactor category usage (replace category in rules)
*/
public function refactor($oldname, $newname)
{
$has_changed = false;
foreach ($this->ruleIterator() as $node) {
$cats = explode(",", (string)$node->category);
if (in_array($oldname, $cats)) {
unset($cats[array_search((string)$oldname, $cats)]);
$cats[] = $newname;
$node->category = implode(",", $cats);
$has_changed = true;
}
}
return $has_changed;
}
/**
* refactor category usage (replace category in rules)
*/
public function isUsed($name)
{
foreach ($this->ruleIterator() as $node) {
if (in_array($name, explode(",", (string)$node->category))) {
return true;
}
}
return false;
}
/**
* collect unique categories from rules and updates the model with auto generated items.
* XXX: if this operation turns out to be a bottleneck, we should move the maintance responsibiliy to the caller
* for the item in question (rule update)
* @return bool true if changed
*/
public function sync()
{
$has_changed = false;
$used_categories = [];
foreach ($this->ruleIterator() as $node) {
foreach (explode(",", (string)$node->category) as $cat) {
if (!in_array($cat, $used_categories)) {
$used_categories[] = $cat;
}
}
}
foreach ($this->categories->category->iterateItems() as $key => $category) {
if (!empty((string)$category->auto) && !in_array((string)$category->name, $used_categories)) {
$this->categories->category->del($key);
@ -102,7 +141,6 @@ class Category extends BaseModel
unset($used_categories[array_search((string)$category->name, $used_categories)]);
}
}
foreach ($used_categories as $name) {
$node = $this->categories->category->add();
$node->name = $name;

View File

@ -0,0 +1,76 @@
{#
# Copyright (c) 2020 Deciso B.V.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES,
# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
# AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
# OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#}
<script>
$( document ).ready(function() {
/*************************************************************************************************************
* link grid actions
*************************************************************************************************************/
$("#grid-categories").UIBootgrid(
{ 'search':'/api/firewall/category/searchItem',
'get':'/api/firewall/category/getItem/',
'set':'/api/firewall/category/setItem/',
'add':'/api/firewall/category/addItem/',
'del':'/api/firewall/category/delItem/'
}
);
});
</script>
<ul class="nav nav-tabs" data-tabs="tabs" id="maintabs">
<li class="active"><a data-toggle="tab" href="#grid-categories">{{ lang._('categories') }}</a></li>
</ul>
<div class="tab-content content-box">
<div id="categories" class="tab-pane fade in active">
<table id="grid-categories" class="table table-condensed table-hover table-striped table-responsive" data-editDialog="DialogEdit">
<thead>
<tr>
<th data-column-id="name" data-type="string">{{ lang._('Name') }}</th>
<th data-column-id="auto" data-width="6em" data-type="string" data-formatter="boolean">{{ lang._('Auto') }}</th>
<th data-column-id="commands" data-width="7em" data-formatter="commands" data-sortable="false">{{ lang._('Commands') }}</th>
<th data-column-id="uuid" data-type="string" data-identifier="true" data-visible="false">{{ lang._('ID') }}</th>
</tr>
</thead>
<tbody>
</tbody>
<tfoot>
<tr>
<td></td>
<td>
<button data-action="add" type="button" class="btn btn-xs btn-default"><span class="fa fa-plus"></span></button>
<button data-action="deleteSelected" type="button" class="btn btn-xs btn-default"><span class="fa fa-trash-o"></span></button>
</td>
</tr>
</tfoot>
</table>
</div>
</div>
{# include dialog #}
{{ partial("layout_partials/base_dialog",['fields':formDialogEdit,'id':'DialogEdit','label':lang._('Edit category')])}}