From bee2f8929f5b18f9daeef49eb0ced1d3eadf9de6 Mon Sep 17 00:00:00 2001 From: Ad Schellevis Date: Wed, 1 Mar 2023 14:47:19 +0100 Subject: [PATCH] Firewall / Aliases - Allow to create firewall rules for logged in OpenVPN user groups. (https://github.com/opnsense/core/issues/6312) (#6367) o extend model with authgroup type (currently only for OpenVPN) o add controller action to list user groups o modify alias form to show group list in a similar way as network groups, simplify some of the code to prevent copying. o add AuthGroup parser to glue the output of list_group_members.php and ovpn_status.py to a set of addresses per group for our new authgroup alias type to use o hook 'learn-address' event in openvpn to trigger an alias update Although theoretically we could pass addresses and common_names from learn-address further in our pipeline, for now we choose to use a common approach which should always offer the correct dataset (also after changing aliases and re-applying them). If for some reason this isn't fast enough, there are always options available to improve the situation, but usually at a cost in terms of complexity. --- src/etc/inc/plugins.inc.d/openvpn.inc | 1 + .../OPNsense/Firewall/Api/AliasController.php | 50 ++++++++++++- .../app/models/OPNsense/Firewall/Alias.xml | 1 + .../Firewall/FieldTypes/AliasContentField.php | 50 ++++++++++++- .../app/views/OPNsense/Firewall/alias.volt | 60 +++++++++------ .../scripts/filter/lib/alias/__init__.py | 3 + src/opnsense/scripts/filter/lib/alias/auth.py | 74 +++++++++++++++++++ src/opnsense/scripts/openvpn/ovpn_event.py | 5 ++ .../OPNsense/Filter/filter_tables.conf | 2 +- 9 files changed, 219 insertions(+), 27 deletions(-) create mode 100644 src/opnsense/scripts/filter/lib/alias/auth.py diff --git a/src/etc/inc/plugins.inc.d/openvpn.inc b/src/etc/inc/plugins.inc.d/openvpn.inc index f15c9c8cf..79112efb7 100644 --- a/src/etc/inc/plugins.inc.d/openvpn.inc +++ b/src/etc/inc/plugins.inc.d/openvpn.inc @@ -666,6 +666,7 @@ function openvpn_reconfigure($mode, $settings, $device_only = false) } if (!empty($settings['authmode'])) { $conf .= "auth-user-pass-verify \"/usr/local/opnsense/scripts/openvpn/ovpn_event.py '{$vpnid}'\" via-env\n"; + $conf .= "learn-address \"/usr/local/opnsense/scripts/openvpn/ovpn_event.py '{$vpnid}'\"\n"; } break; } diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Firewall/Api/AliasController.php b/src/opnsense/mvc/app/controllers/OPNsense/Firewall/Api/AliasController.php index d17dede2b..3d16ea6f6 100644 --- a/src/opnsense/mvc/app/controllers/OPNsense/Firewall/Api/AliasController.php +++ b/src/opnsense/mvc/app/controllers/OPNsense/Firewall/Api/AliasController.php @@ -66,13 +66,32 @@ class AliasController extends ApiMutableModelControllerBase $filter_funct ); - // append category uuid's so we can use these in the frontend - $tmp = []; + /** + * remap some source data from the model as searchBase() is not able to distinct this. + * - category uuid's + * - unix group id's to names in content fields + */ + $categories = []; + $types = []; foreach ($this->getModel()->aliases->alias->iterateItems() as $key => $alias) { - $tmp[$key] = !empty((string)$alias->categories) ? explode(',', (string)$alias->categories) : []; + $categories[$key] = !empty((string)$alias->categories) ? explode(',', (string)$alias->categories) : []; + $types[$key] = (string)$alias->type; } + $group_mapping = null; foreach ($result['rows'] as &$record) { - $record['categories_uuid'] = $tmp[$record['uuid']]; + $record['categories_uuid'] = $categories[$record['uuid']]; + if ($types[$record['uuid']] == 'authgroup') { + if ($group_mapping === null) { + $group_mapping = $this->listUserGroupsAction(); + } + $groups = []; + foreach (explode(',', $record['content']) as $grp) { + if (isset($group_mapping[$grp])) { + $groups[] = $group_mapping[$grp]['name']; + } + } + $record['content'] = implode(',', $groups); + } } return $result; @@ -266,6 +285,29 @@ class AliasController extends ApiMutableModelControllerBase return $result; } + /** + * list user groups + * @return array user groups + */ + public function listUserGroupsAction() + { + $result = []; + $cnf = Config::getInstance()->object(); + if (isset($cnf->system->group)) { + foreach ($cnf->system->group as $group) { + $name = (string)$group->name; + if ($name != 'all') { + $result[(string)$group->gid] = [ + "name" => $name, + "gid" => (string)$group->gid + ]; + } + } + ksort($result); + } + return $result; + } + /** * list network alias types * @return array indexed by country alias name diff --git a/src/opnsense/mvc/app/models/OPNsense/Firewall/Alias.xml b/src/opnsense/mvc/app/models/OPNsense/Firewall/Alias.xml index 78a4284bd..9997dc8f3 100644 --- a/src/opnsense/mvc/app/models/OPNsense/Firewall/Alias.xml +++ b/src/opnsense/mvc/app/models/OPNsense/Firewall/Alias.xml @@ -38,6 +38,7 @@ MAC address BGP ASN Dynamic IPv6 Host + OpenVPN group Internal (automatic) External (advanced) diff --git a/src/opnsense/mvc/app/models/OPNsense/Firewall/FieldTypes/AliasContentField.php b/src/opnsense/mvc/app/models/OPNsense/Firewall/FieldTypes/AliasContentField.php index 54203f08f..413e1bc27 100644 --- a/src/opnsense/mvc/app/models/OPNsense/Firewall/FieldTypes/AliasContentField.php +++ b/src/opnsense/mvc/app/models/OPNsense/Firewall/FieldTypes/AliasContentField.php @@ -32,6 +32,7 @@ use OPNsense\Base\FieldTypes\BaseField; use OPNsense\Base\Validators\CallbackValidator; use OPNsense\Phalcon\Filter\Validation\Validator\Regex; use OPNsense\Phalcon\Filter\Validation\Validator\ExclusionIn; +use OPNsense\Core\Config; use Phalcon\Messages\Message; use OPNsense\Firewall\Util; @@ -54,7 +55,12 @@ class AliasContentField extends BaseField /** * @var array list of known countries */ - private static $internalCountryCodes = array(); + private static $internalCountryCodes = []; + + /** + * @var array list of known user groups + */ + private static $internalAuthGroups = []; /** * item separator @@ -117,6 +123,23 @@ class AliasContentField extends BaseField return self::$internalCountryCodes; } + /** + * fetch valid user groups + * @return array valid groups + */ + public function getUserGroups() + { + if (empty(self::$internalAuthGroups)) { + $cnf = Config::getInstance()->object(); + if (isset($cnf->system->group)) { + foreach ($cnf->system->group as $group) { + self::$internalAuthGroups[(string)$group->gid] = (string)$group->name; + } + } + } + return self::$internalAuthGroups; + } + /** * Validate port alias options * @param array $data to validate @@ -304,6 +327,25 @@ class AliasContentField extends BaseField return $messages; } + /** + * Validate (partial) mac address options + * @param array $data to validate + * @return array + * @throws \OPNsense\Base\ModelException + */ + private function validateGroups($data) + { + $messages = []; + $all_groups = $this->getUserGroups(); + foreach ($this->getItems($data) as $group) { + if (!isset($all_groups[$group])) { + $messages[] = sprintf(gettext('Entry "%s" is not a valid group id.'), $group); + } + } + return $messages; + } + // + /** * retrieve field validators for this field type * @return array @@ -361,6 +403,12 @@ class AliasContentField extends BaseField } ]); break; + case "authgroup": + $validators[] = new CallbackValidator(["callback" => function ($data) { + return $this->validateGroups($data); + } + ]); + break; default: break; } diff --git a/src/opnsense/mvc/app/views/OPNsense/Firewall/alias.volt b/src/opnsense/mvc/app/views/OPNsense/Firewall/alias.volt index 9dadb2abb..fe1d5848b 100644 --- a/src/opnsense/mvc/app/views/OPNsense/Firewall/alias.volt +++ b/src/opnsense/mvc/app/views/OPNsense/Firewall/alias.volt @@ -96,7 +96,7 @@ $("#grid-aliases").bootgrid().on("loaded.rs.jquery.bootgrid", function (e){ // network content field should only contain valid aliases, we need to fetch them separately // since the form field misses context - ajaxGet("/api/firewall/alias/listNetworkAliases", {}, function(data){ + ajaxGet("/api/firewall/alias/list_network_aliases", {}, function(data){ $("#network_content").empty(); $.each(data, function(alias, value) { let $opt = $("