This commit is contained in:
benyamin-codez 2026-02-03 03:34:25 +11:00 committed by GitHub
commit fcbc12abc8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 482 additions and 14 deletions

View file

@ -52,7 +52,11 @@ class AclController extends ApiMutableModelControllerBase
public function delAclAction($uuid)
{
return $this->delBase('acls.acl', $uuid);
$del_tgt = $this->getBase('acl', 'acls.acl', $uuid);
# skip if builtins...
if (!($del_tgt['acl']['name'] == 'any' || $del_tgt['acl']['name'] == 'localnets' || $del_tgt['acl']['name'] == 'localhost' || $del_tgt['acl']['name'] == 'none')) {
return $this->delBase('acls.acl', $uuid);
}
}
public function setAclAction($uuid)

View file

@ -1,18 +1,18 @@
<model>
<mount>//OPNsense/bind/acl</mount>
<description>BIND ACL configuration</description>
<version>1.0.0</version>
<version>1.0.1</version>
<items>
<acls>
<acl type="ArrayField">
<acl type=".\AclField">
<enabled type="BooleanField">
<Default>1</Default>
<Required>Y</Required>
</enabled>
<name type="TextField">
<Required>Y</Required>
<Mask>/^(?!any$|localhost$|localnets$|none$)[0-9a-zA-Z_\-]{1,32}$/u</Mask>
<ValidationMessage>Should be a string between 1 and 32 characters. Allowed characters are 0-9, a-z, A-Z, _ and -. Built-in ACL names must not be used: any, localhost, localnets, none.</ValidationMessage>
<Mask>/^[0-9a-zA-Z_\-]{1,32}$/u</Mask>
<ValidationMessage>Should be a string between 1 and 32 characters. Allowed characters are 0-9, a-z, A-Z, _ and -.</ValidationMessage>
<Constraints>
<check001>
<ValidationMessage>An ACL with this name already exists.</ValidationMessage>
@ -20,7 +20,7 @@
</check001>
</Constraints>
</name>
<networks type="NetworkField">
<networks type=".\AclNetField">
<Required>Y</Required>
<AsList>Y</AsList>
</networks>

View file

@ -1,7 +1,7 @@
<model>
<mount>//OPNsense/bind/domain</mount>
<description>BIND domain configuration</description>
<version>1.1.2</version>
<version>1.1.3</version>
<items>
<domains>
<domain type="ArrayField">
@ -42,7 +42,7 @@
<domainname type="TextField">
<Required>Y</Required>
</domainname>
<allowtransfer type="ModelRelationField">
<allowtransfer type=".\AclModelRelationField">
<Model>
<template>
<source>OPNsense.Bind.Acl</source>
@ -53,7 +53,7 @@
<Multiple>Y</Multiple>
</allowtransfer>
<allowrndctransfer type="BooleanField"/>
<allowquery type="ModelRelationField">
<allowquery type=".\AclModelRelationField">
<Model>
<template>
<source>OPNsense.Bind.Acl</source>

View file

@ -0,0 +1,163 @@
<?php
/*
* Copyright (C) 2025 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\BIND\FieldTypes;
use OPNsense\Base\FieldTypes\ArrayField;
class ACLField extends ArrayField
{
/*
* Extends ArrayField to programmatically add BIND's builtin ACL types to
* the model. The private property $internalTemplateNode is duplicated.
* The actionPostLoadingEvent() method is replaced to add the builtin ACLs
* as child nodes. The ability to add static children is removed. The builtin
* ACL names are defined by the static $builtinNames property. Values for the
* builtin ACLs are populated by the getBuiltinChildren() method. The public
* function add() is also required.
*/
/**
* {@inheritdoc}
*/
private $internalTemplateNode = null;
/**
* @var list to define builtin BIND ACL names
*/
private static $builtinNames = ['none', 'localhost', 'localnets', 'any'];
/**
* @return array of builtin BIND ACLs
*/
protected function getBuiltinChildren()
{
$builtins = [];
foreach (self::$builtinNames as $aclName) {
$builtins [] = [
'enabled' => '1',
'name' => $aclName,
'networks' => 'system derived'
];
}
return $builtins;
}
/**
* {@inheritdoc}
*/
public function add($uuid = null)
{
$nodeUUID = empty($uuid) ? $this->generateUUID() : $uuid;
$container_node = $this->newContainerField($this->__reference . "." . $nodeUUID, $this->internalXMLTagName);
$template_ref = $this->internalTemplateNode->__reference;
foreach ($this->internalTemplateNode->iterateItems() as $key => $node) {
$new_node = clone $node;
$new_node->setInternalReference($container_node->__reference . "." . $key);
$new_node->applyDefault();
$new_node->setChanged();
$container_node->addChildNode($key, $new_node);
if ($node->isContainer()) {
foreach ($node->iterateRecursiveItems() as $subnode) {
if (is_a($subnode, "OPNsense\\Base\\FieldTypes\\ArrayField")) {
// validate child nodes, nesting not supported in this version.
throw new \Exception("Unsupported copy, Array doesn't support nesting.");
}
}
/**
* XXX: incomplete, only supports one nesting level of container fields. In the long run we probably
* should refactor the add() function to push identifiers differently.
*/
foreach ($node->iterateItems() as $subkey => $subnode) {
$new_subnode = clone $subnode;
$new_subnode->setInternalReference($new_node->__reference . "." . $subkey);
$new_subnode->applyDefault();
$new_subnode->setChanged();
$new_node->addChildNode($subkey, $new_subnode);
}
}
}
// make sure we have a UUID on repeating child items
$container_node->setAttributeValue("uuid", $nodeUUID);
// add node to this object
$this->addChildNode($nodeUUID, $container_node);
return $container_node;
}
/**
* {@inheritdoc}
*/
protected function actionPostLoadingEvent()
{
// always make sure there's a node to copy our structure from
if ($this->internalTemplateNode == null) {
$firstKey = array_keys($this->internalChildnodes)[0];
$this->internalTemplateNode = $this->internalChildnodes[$firstKey];
/**
* if first node is empty, remove reference node.
*/
if ($this->internalChildnodes[$firstKey]->getInternalIsVirtual()) {
unset($this->internalChildnodes[$firstKey]);
}
}
// init builtin entries returned by getBuiltinChildren()
foreach (static::getBuiltinChildren() as $skey => $payload) {
$nodeUUID = $this->generateUUID();
$container_node = $this->newContainerField($this->__reference . "." . $nodeUUID, $this->internalXMLTagName);
$container_node->setAttributeValue("uuid", $nodeUUID);
$template_ref = $this->internalTemplateNode->__reference;
foreach ($this->internalTemplateNode->iterateItems() as $key => $value) {
if ($key == 'name') {
foreach ($this->iterateItems() as $pkey => $pnode) {
foreach ($pnode->iterateItems() as $subkey => $subnode) {
if ($subkey == 'name' && $subnode == $payload[$key]) {
// The builtin ACL already exists, let's skip it...
continue 4;
}
}
}
}
$node = clone $value;
$node->setInternalReference($container_node->__reference . "." . $key);
if (isset($payload[$key])) {
$node->setValue($payload[$key]);
}
$node->setChanged();
$container_node->addChildNode($key, $node);
}
$this->addChildNode($nodeUUID, $container_node);
}
}
}

View file

@ -0,0 +1,216 @@
<?php
/*
* Copyright (C) 2015-2025 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\BIND\FieldTypes;
use OPNsense\Base\Validators\CallbackValidator;
use OPNsense\Base\FieldTypes\BaseListField;
use OPNsense\Base\FieldTypes\ModelRelationField;
class AclModelRelationField extends ModelRelationField
{
/*
* Extends ModelRelationField but all private properties and the private
* member loadModelOptions() require duplication. Public methods
* getNodeData() and getValidators() are altered to use the grandparent
* BaseListField:: rather than parent::. The getValidators() method is
* also modified to call new isValidComboSelection() method via new
* CallbackValidator(). We also require public methods actionPostLoadingEvent()
* and setModel() too for this to work.
*/
/**
* {@inheritdoc}
*/
private $internalIsSorted = false;
/**
* {@inheritdoc}
*/
private $mdlStructure = null;
/**
* {@inheritdoc}
*/
private $internalOptionsFromThisModel = false;
/**
* {@inheritdoc}
*/
private $internalCacheKey = "";
/**
* {@inheritdoc}
*/
private static $internalCacheOptionList = [];
/**
* {@inheritdoc}
*/
private static $internalCacheModelStruct = [];
/**
* {@inheritdoc}
*/
private function loadModelOptions($force = false)
{
// only collect options once per source/filter combination, we use a static to save our unique option
// combinations over the running application.
if (!isset(self::$internalCacheOptionList[$this->internalCacheKey]) || $force) {
self::$internalCacheOptionList[$this->internalCacheKey] = [];
foreach ($this->mdlStructure as $modelData) {
// only handle valid model sources
if (!isset($modelData['source']) || !isset($modelData['items']) || !isset($modelData['display'])) {
continue;
}
$className = str_replace('.', '\\', $modelData['source']);
$groupKey = isset($modelData['group']) ? $modelData['group'] : null;
$displayKeys = explode(',', $modelData['display']);
$displayFormat = !empty($modelData['display_format']) ? $modelData['display_format'] : "%s";
$searchItems = $this->getCachedData($className, $modelData['items'], $force);
$groups = [];
foreach ($searchItems as $uuid => $node) {
$descriptions = [];
foreach ($displayKeys as $displayKey) {
$descriptions[] = $node['%' . $displayKey] ?? $node[$displayKey] ?? '';
}
if (isset($modelData['filters'])) {
foreach ($modelData['filters'] as $filterKey => $filterValue) {
$fieldData = $node[$filterKey] ?? null;
if (!preg_match($filterValue, $fieldData) && $fieldData != null) {
continue 2;
}
}
}
if (!empty($groupKey)) {
if (!isset($node[$groupKey]) || isset($groups[$node[$groupKey]])) {
continue;
}
$groups[$node[$groupKey]] = 1;
}
self::$internalCacheOptionList[$this->internalCacheKey][$uuid] = vsprintf(
$displayFormat,
$descriptions
);
}
}
if (!$this->internalIsSorted) {
natcasesort(self::$internalCacheOptionList[$this->internalCacheKey]);
}
}
// Set for use in BaseListField->getNodeData()
$this->internalOptionList = self::$internalCacheOptionList[$this->internalCacheKey];
}
/**
* {@inheritdoc}
*/
public function setModel($mdlStructure)
{
// only handle array type input
if (!is_array($mdlStructure)) {
return;
} else {
$this->mdlStructure = $mdlStructure;
// set internal key for this node based on sources and filter criteria
$this->internalCacheKey = md5(serialize($mdlStructure));
}
}
/**
* {@inheritdoc}
*/
protected function actionPostLoadingEvent()
{
$this->loadModelOptions();
}
/**
* {@inheritdoc}
*/
public function getNodeData()
{
if ($this->internalIsSorted) {
$optKeys = array_merge(explode(',', $this->internalValue), array_keys($this->internalOptionList));
$ordered_option_list = [];
foreach (array_unique($optKeys) as $key) {
if (in_array($key, array_keys($this->internalOptionList))) {
$ordered_option_list[$key] = $this->internalOptionList[$key];
}
}
$this->internalOptionList = $ordered_option_list;
}
return BaseListField::getNodeData();
}
/**
* @param string $input list of ACLs selected to validate
* @return bool if valid combination of ACLs
*/
protected function isValidComboSelection($input)
{
if (strpos($input, ",") !== false) {
// Pass validation if we only have a single-select.
// Otherwise, get the ACL selection data and iterate to see if "any or "none" are included in the multi-select...
$acls = $this->getNodeData();
foreach ($acls as $node => $acl_sel_data) {
if (($acl_sel_data['value'] == 'any' || $acl_sel_data['value'] == 'none') && $acl_sel_data['selected'] == '1') {
$this->setValidationMessage("This ACL cannot be used in combination with others: " . $acl_sel_data['value']);
return false;
}
}
}
return true;
}
/**
* {@inheritdoc}
*/
public function getValidators()
{
// Use validators from BaseListField, includes validations for multi-select, and single-select.
$validators = BaseListField::getValidators();
if ($this->internalValue != null) {
// XXX: may be improved a bit to prevent the same object being constructed multiple times when used
// in different fields (passing of $force parameter)
$this->loadModelOptions($this->internalOptionsFromThisModel);
$that = $this;
$validators[] = new CallbackValidator(["callback" => function ($data) use ($that) {
$messages = [];
if (!$that->isValidComboSelection($data)) {
$messages[] = $this->getValidationMessage();
}
return $messages;
}]);
}
return $validators;
}
}

View file

@ -0,0 +1,64 @@
<?php
/*
* Copyright (C) 2025 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\BIND\FieldTypes;
use OPNsense\Base\Validators\CallbackValidator;
use OPNsense\Base\FieldTypes\BaseSetField;
use OPNsense\Base\FieldTypes\NetworkField;
class ACLNetField extends NetworkField
{
/*
* Extends the NetworkField getValidators() method to ignore networks specified
* as 'system defined', which is the value used to describe BIND's builtin ACLs.
*/
/**
* {@inheritdoc}
*/
public function getValidators()
{
$validators = BaseSetField::getValidators();
if ($this->internalValue != null) {
if ($this->internalValue != "any" || $this->internalWildcardEnabled == false) {
$that = $this;
$validators[] = new CallbackValidator(["callback" => function ($data) use ($that) {
$messages = [];
if ($data == 'system derived' ) {
// ignoring builtin BIND ACL names
} elseif (!$that->isValidInput($data)) {
$messages[] = $this->getValidationMessage();
}
return $messages;
}]);
}
}
return $validators;
}
}

View file

@ -1,7 +1,7 @@
<model>
<mount>//OPNsense/bind/general</mount>
<description>BIND configuration</description>
<version>1.0.12</version>
<version>1.0.13</version>
<items>
<enabled type="BooleanField">
<Default>0</Default>
@ -86,7 +86,7 @@
<MaximumValue>99</MaximumValue>
<ValidationMessage>Choose a value between 1 and 99.</ValidationMessage>
</maxcachesize>
<recursion type="ModelRelationField">
<recursion type=".\AclModelRelationField">
<Model>
<template>
<source>OPNsense.Bind.Acl</source>
@ -97,7 +97,7 @@
<Multiple>Y</Multiple>
<ValidationMessage>Choose an ACL.</ValidationMessage>
</recursion>
<allowtransfer type="ModelRelationField">
<allowtransfer type=".\AclModelRelationField">
<Model>
<template>
<source>OPNsense.Bind.Acl</source>
@ -107,7 +107,7 @@
</Model>
<Multiple>Y</Multiple>
</allowtransfer>
<allowquery type="ModelRelationField">
<allowquery type=".\AclModelRelationField">
<Model>
<template>
<source>OPNsense.Bind.Acl</source>

View file

@ -397,7 +397,26 @@ $(document).ready(function() {
'set': '/api/bind/acl/set_acl/',
'add': '/api/bind/acl/add_acl/',
'del': '/api/bind/acl/del_acl/',
'toggle': '/api/bind/acl/toggle_acl/'
'toggle': '/api/bind/acl/toggle_acl/',
options: {
formatters: {
"commands": function (column, row) {
// Disable the command buttons for builtin ACLs
if (row.networks === "system derived") {
return "<button type=\"button\" class=\"btn btn-xs btn-default bootgrid-tooltip command-edit\" data-row-id=\"" + row.uuid + "\" title=\"\" aria-label=\"Edit\" data-original-title=\"Edit\" disabled=\"disabled\"><span class=\"fa fa-fw fa-pencil\"></span></button> " +
"<button type=\"button\" class=\"btn btn-xs btn-default bootgrid-tooltip command-copy\" data-row-id=\"" + row.uuid + "\" title=\"\" aria-label=\"Clone\" data-original-title=\"Clone\" disabled=\"disabled\"><span class=\"fa fa-fw fa-clone\"></span></button> " +
"<button type=\"button\" class=\"btn btn-xs btn-default bootgrid-tooltip command-delete\" data-row-id=\"" + row.uuid + "\" title=\"\" aria-label=\"Delete\" data-original-title=\"Delete\" disabled=\"disabled\"><span class=\"fa fa-fw fa-trash-o\"></span></button>";
} else {
return "<button type=\"button\" class=\"btn btn-xs btn-default bootgrid-tooltip command-edit\" data-row-id=\"" + row.uuid + "\" title=\"\" aria-label=\"Edit\" data-original-title=\"Edit\"><span class=\"fa fa-fw fa-pencil\"></span></button> " +
"<button type=\"button\" class=\"btn btn-xs btn-default bootgrid-tooltip command-copy\" data-row-id=\"" + row.uuid + "\" title=\"\" aria-label=\"Clone\" data-original-title=\"Clone\"><span class=\"fa fa-fw fa-clone\"></span></button> " +
"<button type=\"button\" class=\"btn btn-xs btn-default bootgrid-tooltip command-delete\" data-row-id=\"" + row.uuid + "\" title=\"\" aria-label=\"Delete\" data-original-title=\"Delete\"><span class=\"fa fa-fw fa-trash-o\"></span></button>";
}
}
}
}
}).on("loaded.rs.jquery.bootgrid", function(e) {
// always save on load to ensure the builtin ACLs are in config.xml
saveFormToEndpoint(url = "/api/bind/acl/set", formid = 'frm_general_settings');
});
$("#grid-primary-domains").UIBootgrid({

View file

@ -3,7 +3,9 @@
{% if helpers.exists('OPNsense.bind.acl.acls.acl') %}
{% for acl_list in helpers.toList('OPNsense.bind.acl.acls.acl') %}
{% if acl_list.enabled == '1' %}
{% if not (acl_list.name == 'none' or acl_list.name == 'localhost' or acl_list.name == 'localnets' or acl_list.name == 'any') %}
acl "{{ acl_list.name }}" { {{ acl_list.networks.replace(',', '; ') }}; };
{% endif %}
{% endif %}
{% endfor %}
{% endif %}