mirror of
https://github.com/lucaspalomodevelop/core.git
synced 2026-03-15 00:54:41 +00:00
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.
This commit is contained in:
parent
0aa9e0bea0
commit
bee2f8929f
@ -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;
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -38,6 +38,7 @@
|
||||
<mac>MAC address</mac>
|
||||
<asn>BGP ASN</asn>
|
||||
<dynipv6host>Dynamic IPv6 Host</dynipv6host>
|
||||
<authgroup>OpenVPN group</authgroup>
|
||||
<internal>Internal (automatic)</internal>
|
||||
<external>External (advanced)</external>
|
||||
</OptionValues>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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 = $("<option/>").val(alias).text(value.name);
|
||||
@ -250,14 +250,30 @@
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* fetch user groups
|
||||
**/
|
||||
ajaxGet("/api/firewall/alias/list_user_groups", {}, function(data){
|
||||
$("#authgroup_content").empty();
|
||||
$.each(data, function(alias, value) {
|
||||
let $opt = $("<option/>").val(alias).text(value.name);
|
||||
$opt.data('subtext', value.description);
|
||||
$("#authgroup_content").append($opt);
|
||||
});
|
||||
$("#authgroup_content").selectpicker('refresh');
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* hook network group type changes, replicate content
|
||||
*/
|
||||
$("#network_content").change(function(){
|
||||
$("#network_content, #authgroup_content").change(function(){
|
||||
let target = $(this);
|
||||
console.log(target);
|
||||
let $content = $("#alias\\.content");
|
||||
$content.unbind('tokenize:tokens:change');
|
||||
$content.tokenize2().trigger('tokenize:clear');
|
||||
$("#network_content").each(function () {
|
||||
target.each(function () {
|
||||
$.each($(this).val(), function(key, item){
|
||||
$content.tokenize2().trigger('tokenize:tokens:add', item);
|
||||
});
|
||||
@ -270,7 +286,6 @@
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* Type selector, show correct type input.
|
||||
*/
|
||||
@ -280,6 +295,10 @@
|
||||
$("#row_alias\\.interface").hide();
|
||||
$("#copy-paste").hide();
|
||||
switch ($(this).val()) {
|
||||
case 'authgroup':
|
||||
$("#alias_type_authgroup").show();
|
||||
$("#alias\\.proto").selectpicker('hide');
|
||||
break;
|
||||
case 'geoip':
|
||||
$("#alias_type_geoip").show();
|
||||
$("#alias\\.proto").selectpicker('show');
|
||||
@ -321,24 +340,18 @@
|
||||
*/
|
||||
$("#alias\\.content").change(function(){
|
||||
var items = $(this).val();
|
||||
$(".geoip_select").each(function(){
|
||||
var geo_item = $(this);
|
||||
geo_item.val([]);
|
||||
for (var i=0; i < items.length; ++i) {
|
||||
geo_item.find('option[value="' + $.escapeSelector(items[i]) + '"]').prop("selected", true);
|
||||
}
|
||||
|
||||
['#authgroup_content', '#network_content', '.geoip_select'].forEach(function(target){
|
||||
console.log(target);
|
||||
$(target).each(function(){
|
||||
var content_item = $(this);
|
||||
content_item.val([]);
|
||||
for (var i=0; i < items.length; ++i) {
|
||||
content_item.find('option[value="' + $.escapeSelector(items[i]) + '"]').prop("selected", true);
|
||||
}
|
||||
});
|
||||
$(target).selectpicker('refresh');
|
||||
});
|
||||
$(".geoip_select").selectpicker('refresh');
|
||||
geoip_update_labels();
|
||||
$("#network_content").each(function(){
|
||||
var network_item = $(this);
|
||||
network_item.val([]);
|
||||
for (var i=0; i < items.length; ++i) {
|
||||
network_item.find('option[value="' + $.escapeSelector(items[i]) + '"]').prop("selected", true);
|
||||
}
|
||||
});
|
||||
$("#network_content").selectpicker('refresh');
|
||||
});
|
||||
|
||||
/**
|
||||
@ -574,7 +587,7 @@
|
||||
<div class="hidden">
|
||||
<!-- filter per type container -->
|
||||
<div id="type_filter_container" class="btn-group">
|
||||
<select id="type_filter" data-title="{{ lang._('Filter type') }}" class="selectpicker" multiple="multiple" data-width="200px">
|
||||
<select id="type_filter" data-title="{{ lang._('Filter type') }}" class="selectpicker" data-live-search="true" multiple="multiple" data-width="200px">
|
||||
<option value="host">{{ lang._('Host(s)') }}</option>
|
||||
<option value="network">{{ lang._('Network(s)') }}</option>
|
||||
<option value="port">{{ lang._('Port(s)') }}</option>
|
||||
@ -585,6 +598,7 @@
|
||||
<option value="mac">{{ lang._('MAC address') }}</option>
|
||||
<option value="asn">{{ lang._('BGP ASN') }}</option>
|
||||
<option value="dynipv6host">{{ lang._('Dynamic IPv6 Host') }}</option>
|
||||
<option value="authgroup">{{ lang._('(OpenVPN) user groups') }}</option>
|
||||
<option value="internal">{{ lang._('Internal (automatic)') }}</option>
|
||||
<option value="external">{{ lang._('External (advanced)') }}</option>
|
||||
</select>
|
||||
@ -813,6 +827,10 @@
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="alias_type" id="alias_type_authgroup" style="display: none;">
|
||||
<select multiple="multiple" class="selectpicker" id="authgroup_content" data-live-search="true">
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<a href="#" class="text-danger" id="clear-options_alias.content"><i class="fa fa-times-circle"></i>
|
||||
<small>{{lang._('Clear All')}}</small></a><span id="copy-paste">
|
||||
|
||||
@ -38,6 +38,7 @@ from .uri import UriParser
|
||||
from .arpcache import ArpCache
|
||||
from .bgpasn import BGPASN
|
||||
from .interface import InterfaceParser
|
||||
from .auth import AuthGroup
|
||||
from .base import BaseContentParser
|
||||
|
||||
|
||||
@ -215,6 +216,8 @@ class Alias(object):
|
||||
return ArpCache(**self._properties)
|
||||
elif self._type == 'asn':
|
||||
return BGPASN(**self._properties)
|
||||
elif self._type == 'authgroup':
|
||||
return AuthGroup(**self._properties)
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
74
src/opnsense/scripts/filter/lib/alias/auth.py
Normal file
74
src/opnsense/scripts/filter/lib/alias/auth.py
Normal file
@ -0,0 +1,74 @@
|
||||
"""
|
||||
Copyright (c) 2023 Ad Schellevis <ad@opnsense.org>
|
||||
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.
|
||||
|
||||
"""
|
||||
import subprocess
|
||||
import syslog
|
||||
import ujson
|
||||
from .base import BaseContentParser
|
||||
|
||||
|
||||
class AuthGroup(BaseContentParser):
|
||||
_auth_db = None
|
||||
|
||||
@classmethod
|
||||
def _update(cls):
|
||||
cls._auth_db = {}
|
||||
try:
|
||||
params = ['/usr/local/opnsense/scripts/auth/list_group_members.php']
|
||||
group_members = ujson.loads(subprocess.run(params, capture_output=True, text=True).stdout.strip())
|
||||
except (ValueError, FileNotFoundError):
|
||||
syslog.syslog(syslog.LOG_ERR, 'error fetching group members (%s)' % " ".join(params))
|
||||
return
|
||||
try:
|
||||
params = ['/usr/local/opnsense/scripts/openvpn/ovpn_status.py', '--options', 'server']
|
||||
ovpn_status = ujson.loads(subprocess.run(params, capture_output=True,text=True).stdout.strip())
|
||||
except (ValueError, FileNotFoundError):
|
||||
syslog.syslog(syslog.LOG_ERR, 'error fetching openvpn clients (%s)' % " ".join(params))
|
||||
return
|
||||
|
||||
users = {}
|
||||
for server in ovpn_status.get('server', None):
|
||||
if server:
|
||||
for client in ovpn_status['server'][server].get('client_list', []):
|
||||
if type(client) is dict and 'common_name' in client:
|
||||
if client['common_name'] not in users:
|
||||
users[client['common_name']] = []
|
||||
if client.get('virtual_address', '') != '':
|
||||
users[client['common_name']].append(client.get('virtual_address'))
|
||||
if client.get('virtual_ipv6_address', '') != '':
|
||||
users[client['common_name']].append(client.get('virtual_ipv6_address'))
|
||||
for grp in group_members:
|
||||
cls._auth_db[grp] = []
|
||||
for user in group_members[grp].get('members', []):
|
||||
for address in users.get(user, []):
|
||||
cls._auth_db[grp].append(address)
|
||||
|
||||
def iter_addresses(self, group):
|
||||
if self._auth_db is None:
|
||||
self._update()
|
||||
if group in self._auth_db:
|
||||
for address in self._auth_db[group]:
|
||||
yield address
|
||||
@ -66,6 +66,11 @@ def main(params):
|
||||
sys.exit(subprocess.run("%s/client_connect.php" % cmd_path).returncode)
|
||||
elif params.script_type == 'client-disconnect':
|
||||
sys.exit(subprocess.run("%s/client_disconnect.sh" % cmd_path).returncode)
|
||||
elif params.script_type == 'learn-address':
|
||||
if os.fork() == 0:
|
||||
sys.exit(subprocess.run(
|
||||
['/usr/local/opnsense/scripts/filter/update_tables.py', '--types', 'authgroup']
|
||||
).returncode)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@ -34,7 +34,7 @@
|
||||
<ttl>{{ system.aliasesresolveinterval|default('300') }}</ttl>
|
||||
{% elif alias.type == 'mac' %}
|
||||
<ttl>30</ttl>
|
||||
{% elif alias.type == 'dynipv6host' %}
|
||||
{% elif alias.type in ['dynipv6host', 'authgroup'] %}
|
||||
<ttl>1</ttl>
|
||||
{% endif %}
|
||||
</table>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user