From 9a0ca36dc39625578f5630733a49813e496c9fbf Mon Sep 17 00:00:00 2001 From: Frank Wall Date: Fri, 9 Nov 2018 13:41:34 +0100 Subject: [PATCH 1/5] net/haproxy: add user/group management, refs #300 --- .../HAProxy/Api/SettingsController.php | 261 ++++++++++++++++++ .../OPNsense/HAProxy/IndexController.php | 2 + .../OPNsense/HAProxy/forms/dialogGroup.xml | 27 ++ .../OPNsense/HAProxy/forms/dialogUser.xml | 26 ++ .../app/models/OPNsense/HAProxy/HAProxy.xml | 61 +++- .../app/models/OPNsense/HAProxy/Menu/Menu.xml | 2 + .../mvc/app/views/OPNsense/HAProxy/index.volt | 126 +++++++++ 7 files changed, 504 insertions(+), 1 deletion(-) create mode 100644 net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/dialogGroup.xml create mode 100644 net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/dialogUser.xml diff --git a/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/Api/SettingsController.php b/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/Api/SettingsController.php index e8fb42d1b..f93f514e9 100644 --- a/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/Api/SettingsController.php +++ b/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/Api/SettingsController.php @@ -1103,4 +1103,265 @@ class SettingsController extends ApiControllerBase "name" ); } + + /** + * retrieve group settings or return defaults + * @param $uuid item unique id + * @return array + */ + public function getGroupAction($uuid = null) + { + $mdlCP = new HAProxy(); + if ($uuid != null) { + $node = $mdlCP->getNodeByReference('groups.group.'.$uuid); + if ($node != null) { + // return node + return array("group" => $node->getNodes()); + } + } else { + // generate new node, but don't save to disc + $node = $mdlCP->groups->group->add(); + return array("group" => $node->getNodes()); + } + return array(); + } + + /** + * update group with given properties + * @param $uuid item unique id + * @return array + */ + public function setGroupAction($uuid) + { + if ($this->request->isPost() && $this->request->hasPost("group")) { + $mdlCP = new HAProxy(); + if ($uuid != null) { + $node = $mdlCP->getNodeByReference('groups.group.'.$uuid); + if ($node != null) { + $node->setNodes($this->request->getPost("group")); + return $this->save($mdlCP, $node, "group"); + } + } + } + return array("result"=>"failed"); + } + + /** + * add new group and set with attributes from post + * @return array + */ + public function addGroupAction() + { + $result = array("result"=>"failed"); + if ($this->request->isPost() && $this->request->hasPost("group")) { + $mdlCP = new HAProxy(); + $node = $mdlCP->groups->group->Add(); + $node->setNodes($this->request->getPost("group")); + return $this->save($mdlCP, $node, "group"); + } + return $result; + } + + /** + * delete group by uuid + * @param $uuid item unique id + * @return array status + */ + public function delGroupAction($uuid) + { + $result = array("result"=>"failed"); + if ($this->request->isPost()) { + $mdlCP = new HAProxy(); + if ($uuid != null) { + if ($mdlCP->groups->group->del($uuid)) { + // if item is removed, serialize to config and save + $mdlCP->serializeToConfig(); + Config::getInstance()->save(); + $result['result'] = 'deleted'; + } else { + $result['result'] = 'not found'; + } + } + } + return $result; + } + + /** + * toggle group by uuid (enable/disable) + * @param $uuid item unique id + * @param $enabled desired state enabled(1)/disabled(0), leave empty for toggle + * @return array status + */ + public function toggleGroupAction($uuid, $enabled = null) + { + + $result = array("result" => "failed"); + if ($this->request->isPost()) { + $mdlCP = new HAProxy(); + if ($uuid != null) { + $node = $mdlCP->getNodeByReference('groups.group.' . $uuid); + if ($node != null) { + if ($enabled == "0" || $enabled == "1") { + $node->enabled = (string)$enabled; + } elseif ((string)$node->enabled == "1") { + $node->enabled = "0"; + } else { + $node->enabled = "1"; + } + $result['result'] = $node->enabled; + // if item has toggled, serialize to config and save + $mdlCP->serializeToConfig(); + Config::getInstance()->save(); + } + } + } + return $result; + } + + /** + * search groups + * @return array + */ + public function searchGroupsAction() + { + $this->sessionClose(); + $mdlCP = new HAProxy(); + $grid = new UIModelGrid($mdlCP->groups->group); + return $grid->fetchBindRequest( + $this->request, + array("enabled", "name", "description"), + "name" + ); + } + + /** + * retrieve user settings or return defaults + * @param $uuid item unique id + * @return array + */ + public function getUserAction($uuid = null) + { + $mdlCP = new HAProxy(); + if ($uuid != null) { + $node = $mdlCP->getNodeByReference('users.user.'.$uuid); + if ($node != null) { + // return node + return array("user" => $node->getNodes()); + } + } else { + // generate new node, but don't save to disc + $node = $mdlCP->users->user->add(); + return array("user" => $node->getNodes()); + } + return array(); + } + + /** + * update user with given properties + * @param $uuid item unique id + * @return array + */ + public function setUserAction($uuid) + { + if ($this->request->isPost() && $this->request->hasPost("user")) { + $mdlCP = new HAProxy(); + if ($uuid != null) { + $node = $mdlCP->getNodeByReference('users.user.'.$uuid); + if ($node != null) { + $node->setNodes($this->request->getPost("user")); + return $this->save($mdlCP, $node, "user"); + } + } + } + return array("result"=>"failed"); + } + + /** + * add new user and set with attributes from post + * @return array + */ + public function addUserAction() + { + $result = array("result"=>"failed"); + if ($this->request->isPost() && $this->request->hasPost("user")) { + $mdlCP = new HAProxy(); + $node = $mdlCP->users->user->Add(); + $node->setNodes($this->request->getPost("user")); + return $this->save($mdlCP, $node, "user"); + } + return $result; + } + + /** + * delete user by uuid + * @param $uuid item unique id + * @return array status + */ + public function delUserAction($uuid) + { + $result = array("result"=>"failed"); + if ($this->request->isPost()) { + $mdlCP = new HAProxy(); + if ($uuid != null) { + if ($mdlCP->users->user->del($uuid)) { + // if item is removed, serialize to config and save + $mdlCP->serializeToConfig(); + Config::getInstance()->save(); + $result['result'] = 'deleted'; + } else { + $result['result'] = 'not found'; + } + } + } + return $result; + } + + /** + * toggle user by uuid (enable/disable) + * @param $uuid item unique id + * @param $enabled desired state enabled(1)/disabled(0), leave empty for toggle + * @return array status + */ + public function toggleUserAction($uuid, $enabled = null) + { + + $result = array("result" => "failed"); + if ($this->request->isPost()) { + $mdlCP = new HAProxy(); + if ($uuid != null) { + $node = $mdlCP->getNodeByReference('users.user.' . $uuid); + if ($node != null) { + if ($enabled == "0" || $enabled == "1") { + $node->enabled = (string)$enabled; + } elseif ((string)$node->enabled == "1") { + $node->enabled = "0"; + } else { + $node->enabled = "1"; + } + $result['result'] = $node->enabled; + // if item has toggled, serialize to config and save + $mdlCP->serializeToConfig(); + Config::getInstance()->save(); + } + } + } + return $result; + } + + /** + * search users + * @return array + */ + public function searchUsersAction() + { + $this->sessionClose(); + $mdlCP = new HAProxy(); + $grid = new UIModelGrid($mdlCP->users->user); + return $grid->fetchBindRequest( + $this->request, + array("enabled", "name", "description"), + "name" + ); + } + } diff --git a/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/IndexController.php b/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/IndexController.php index 265d1734d..604f1d4c7 100644 --- a/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/IndexController.php +++ b/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/IndexController.php @@ -51,6 +51,8 @@ class IndexController extends \OPNsense\Base\IndexController $this->view->formDialogHealthcheck = $this->getForm("dialogHealthcheck"); $this->view->formDialogAction = $this->getForm("dialogAction"); $this->view->formDialogAcl = $this->getForm("dialogAcl"); + $this->view->formDialogUser = $this->getForm("dialogUser"); + $this->view->formDialogGroup = $this->getForm("dialogGroup"); $this->view->formDialogLua = $this->getForm("dialogLua"); $this->view->formDialogErrorfile = $this->getForm("dialogErrorfile"); $this->view->formDialogMapfile = $this->getForm("dialogMapfile"); diff --git a/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/dialogGroup.xml b/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/dialogGroup.xml new file mode 100644 index 000000000..d868a472d --- /dev/null +++ b/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/dialogGroup.xml @@ -0,0 +1,27 @@ +
+ + group.enabled + + checkbox + Enable this group. + + + group.name + + text + Name to identify this group. + + + group.description + + text + Description for this group. + + + group.members + + select_multiple + true + Type username or choose from list. + +
diff --git a/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/dialogUser.xml b/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/dialogUser.xml new file mode 100644 index 000000000..000bd9a65 --- /dev/null +++ b/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/dialogUser.xml @@ -0,0 +1,26 @@ +
+ + user.enabled + + checkbox + Enable this user. + + + user.name + + text + Name to identify this user. + + + user.description + + text + Description for this user. + + + user.password + + password +
NOTE: Avoid using unencrypted passwords that start with a $-sign, because this indicates an encrypted password and will make it impossible to authenticate.
]]>
+
+
diff --git a/net/haproxy/src/opnsense/mvc/app/models/OPNsense/HAProxy/HAProxy.xml b/net/haproxy/src/opnsense/mvc/app/models/OPNsense/HAProxy/HAProxy.xml index 3ada067d2..16d993616 100644 --- a/net/haproxy/src/opnsense/mvc/app/models/OPNsense/HAProxy/HAProxy.xml +++ b/net/haproxy/src/opnsense/mvc/app/models/OPNsense/HAProxy/HAProxy.xml @@ -1,6 +1,6 @@ //OPNsense/HAProxy - 2.5.0 + 2.6.0 the HAProxy load balancer @@ -1921,5 +1921,64 @@ + + + + Y + + + 1 + Y + + + /^[^\t^,^;^\.^\[^\]^\{^\}]{1,255}$/u + Should be a string between 1 and 255 characters. + Y + + + /^.{1,255}$/u + Should be a string between 1 and 255 characters. + N + + + + + + Related user not found + Y + N + + + + + + + Y + + + 1 + Y + + + /^[^\t^,^;^\.^\[^\]^\{^\}]{1,255}$/u + Should be a string between 1 and 255 characters. + Y + + + /^.{1,255}$/u + Should be a string between 1 and 255 characters. + N + + + /^.{1,512}$/u + Should be a string between 1 and 512 characters. + Y + + + diff --git a/net/haproxy/src/opnsense/mvc/app/models/OPNsense/HAProxy/Menu/Menu.xml b/net/haproxy/src/opnsense/mvc/app/models/OPNsense/HAProxy/Menu/Menu.xml index 3747da2ca..8e11c33d5 100644 --- a/net/haproxy/src/opnsense/mvc/app/models/OPNsense/HAProxy/Menu/Menu.xml +++ b/net/haproxy/src/opnsense/mvc/app/models/OPNsense/HAProxy/Menu/Menu.xml @@ -14,6 +14,8 @@ + + diff --git a/net/haproxy/src/opnsense/mvc/app/views/OPNsense/HAProxy/index.volt b/net/haproxy/src/opnsense/mvc/app/views/OPNsense/HAProxy/index.volt index 6148888a7..b6c9479b1 100644 --- a/net/haproxy/src/opnsense/mvc/app/views/OPNsense/HAProxy/index.volt +++ b/net/haproxy/src/opnsense/mvc/app/views/OPNsense/HAProxy/index.volt @@ -121,6 +121,32 @@ POSSIBILITY OF SUCH DAMAGE. } ); + $("#grid-users").UIBootgrid( + { search:'/api/haproxy/settings/searchUsers', + get:'/api/haproxy/settings/getUser/', + set:'/api/haproxy/settings/setUser/', + add:'/api/haproxy/settings/addUser/', + del:'/api/haproxy/settings/delUser/', + toggle:'/api/haproxy/settings/toggleUser/', + options: { + rowCount:[10,25,50,100,500,1000] + } + } + ); + + $("#grid-groups").UIBootgrid( + { search:'/api/haproxy/settings/searchGroups', + get:'/api/haproxy/settings/getGroup/', + set:'/api/haproxy/settings/setGroup/', + add:'/api/haproxy/settings/addGroup/', + del:'/api/haproxy/settings/delGroup/', + toggle:'/api/haproxy/settings/toggleGroup/', + options: { + rowCount:[10,25,50,100,500,1000] + } + } + ); + $("#grid-luas").UIBootgrid( { search:'/api/haproxy/settings/searchLuas', get:'/api/haproxy/settings/getLua/', @@ -439,6 +465,20 @@ POSSIBILITY OF SUCH DAMAGE. + + {# add automatically generated tabs #} {% for tab in mainForm['tabs']|default([]) %} {% if tab['subtabs']|default(false) %} @@ -541,6 +581,20 @@ POSSIBILITY OF SUCH DAMAGE. +
+
+

{{ lang._('User Management') }}

+

{{ lang._("Optionally HAProxy manages an internal list of users and groups, which can be used for HTTP Basic Authentication as well as access to HAProxy's internal statistic pages.") }}

+
    +
  • {{ lang._('%sUser:%s A username/password combination. Both secure (encrypted) and insecure (unencrypted) passwords can be used.') | format('', '') }}
  • +
  • {{ lang._('%sGroup:%s A optional list containing one or more users. Groups usually make it easier to manage permissions for a large number of users') | format('', '') }}
  • +
+

{{ lang._('Note that users and groups must be selected from the Backend Pool or Public Service configuration in order to be used for authentication.') }}

+

{{ lang._("For more information on HAProxy's %suser/group management%s see the %sofficial documentation%s.") | format('', '', '', '') }}

+
+
+
+

{{ lang._('Advanced Features') }}

@@ -780,6 +834,76 @@ POSSIBILITY OF SUCH DAMAGE.
+
+ + + + + + + + + + + + + + + + + + + + +
{{ lang._('Enabled') }}{{ lang._('User ID') }}{{ lang._('Username') }}{{ lang._('Description') }}{{ lang._('Commands') }}{{ lang._('ID') }}
+ + +
+ +
+
+ + +
+
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + +
{{ lang._('Enabled') }}{{ lang._('Group ID') }}{{ lang._('Group') }}{{ lang._('Description') }}{{ lang._('Commands') }}{{ lang._('ID') }}
+ + +
+ +
+
+ + +
+
+
+
+
@@ -891,6 +1015,8 @@ POSSIBILITY OF SUCH DAMAGE. {{ partial("layout_partials/base_dialog",['fields':formDialogHealthcheck,'id':'DialogHealthcheck','label':lang._('Edit Health Monitor')])}} {{ partial("layout_partials/base_dialog",['fields':formDialogAction,'id':'DialogAction','label':lang._('Edit Rule')])}} {{ partial("layout_partials/base_dialog",['fields':formDialogAcl,'id':'DialogAcl','label':lang._('Edit Condition')])}} +{{ partial("layout_partials/base_dialog",['fields':formDialogUser,'id':'DialogUser','label':lang._('Edit User')])}} +{{ partial("layout_partials/base_dialog",['fields':formDialogGroup,'id':'DialogGroup','label':lang._('Edit Group')])}} {{ partial("layout_partials/base_dialog",['fields':formDialogLua,'id':'DialogLua','label':lang._('Edit Lua Script')])}} {{ partial("layout_partials/base_dialog",['fields':formDialogErrorfile,'id':'DialogErrorfile','label':lang._('Edit Error Message')])}} {{ partial("layout_partials/base_dialog",['fields':formDialogMapfile,'id':'DialogMapfile','label':lang._('Edit Map File')])}} From 6914f13ae8503b762530cf93125212d5a686a82f Mon Sep 17 00:00:00 2001 From: Frank Wall Date: Fri, 9 Nov 2018 16:56:38 +0100 Subject: [PATCH 2/5] net/haproxy: add support for HTTP Basic Auth, closes #300 --- .../OPNsense/HAProxy/forms/dialogBackend.xml | 25 +++ .../OPNsense/HAProxy/forms/dialogFrontend.xml | 25 +++ .../app/models/OPNsense/HAProxy/HAProxy.xml | 56 ++++++ .../mvc/app/views/OPNsense/HAProxy/index.volt | 2 +- .../templates/OPNsense/HAProxy/haproxy.conf | 166 ++++++++++++++++++ 5 files changed, 273 insertions(+), 1 deletion(-) diff --git a/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/dialogBackend.xml b/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/dialogBackend.xml index bfb59e8e9..744615c4a 100644 --- a/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/dialogBackend.xml +++ b/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/dialogBackend.xml @@ -223,6 +223,31 @@ true + + + header + + + + backend.basicAuthEnabled + + checkbox + + + + backend.basicAuthUsers + + select_multiple + true + Type username or choose from list. + + + backend.basicAuthGroups + + select_multiple + true + Type group or choose from list. + header diff --git a/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/dialogFrontend.xml b/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/dialogFrontend.xml index af4f53a11..6372012b2 100644 --- a/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/dialogFrontend.xml +++ b/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/dialogFrontend.xml @@ -161,6 +161,31 @@ To import additional CRLs, go to CRL Manager.]]> Type CRL name or choose from list. + + + header + + + + frontend.basicAuthEnabled + + checkbox + + + + frontend.basicAuthUsers + + select_multiple + true + Type username or choose from list. + + + frontend.basicAuthGroups + + select_multiple + true + Type group or choose from list. + header diff --git a/net/haproxy/src/opnsense/mvc/app/models/OPNsense/HAProxy/HAProxy.xml b/net/haproxy/src/opnsense/mvc/app/models/OPNsense/HAProxy/HAProxy.xml index 16d993616..72bd1637f 100644 --- a/net/haproxy/src/opnsense/mvc/app/models/OPNsense/HAProxy/HAProxy.xml +++ b/net/haproxy/src/opnsense/mvc/app/models/OPNsense/HAProxy/HAProxy.xml @@ -429,6 +429,34 @@ Y Please select a valid CA from the list. + + 0 + N + + + + + + Related user not found + Y + N + + + + + + Related group not found + Y + N + 1 500000 @@ -830,6 +858,34 @@ Should be a number between 1 and 8 characters, optionally followed by either "d", "h", "m", "s", "ms" or "us". N + + 0 + N + + + + + + Related user not found + Y + N + + + + + + Related group not found + Y + N + /^([0-9]{1,8}(?:us|ms|s|m|h|d)?)/u Should be a number between 1 and 8 characters, optionally followed by either "d", "h", "m", "s", "ms" or "us". diff --git a/net/haproxy/src/opnsense/mvc/app/views/OPNsense/HAProxy/index.volt b/net/haproxy/src/opnsense/mvc/app/views/OPNsense/HAProxy/index.volt index b6c9479b1..f97a8f6ae 100644 --- a/net/haproxy/src/opnsense/mvc/app/views/OPNsense/HAProxy/index.volt +++ b/net/haproxy/src/opnsense/mvc/app/views/OPNsense/HAProxy/index.volt @@ -589,7 +589,7 @@ POSSIBILITY OF SUCH DAMAGE.
  • {{ lang._('%sUser:%s A username/password combination. Both secure (encrypted) and insecure (unencrypted) passwords can be used.') | format('', '') }}
  • {{ lang._('%sGroup:%s A optional list containing one or more users. Groups usually make it easier to manage permissions for a large number of users') | format('', '') }}
  • -

    {{ lang._('Note that users and groups must be selected from the Backend Pool or Public Service configuration in order to be used for authentication.') }}

    +

    {{ lang._('Note that users and groups must be selected from the Backend Pool or Public Service configuration in order to be used for authentication. In addition to this users and groups may also be used in Rules/Conditions.') }}

    {{ lang._("For more information on HAProxy's %suser/group management%s see the %sofficial documentation%s.") | format('', '', '', '') }}


    diff --git a/net/haproxy/src/opnsense/service/templates/OPNsense/HAProxy/haproxy.conf b/net/haproxy/src/opnsense/service/templates/OPNsense/HAProxy/haproxy.conf index 9565bd8a2..784d4d06b 100644 --- a/net/haproxy/src/opnsense/service/templates/OPNsense/HAProxy/haproxy.conf +++ b/net/haproxy/src/opnsense/service/templates/OPNsense/HAProxy/haproxy.conf @@ -623,6 +623,110 @@ {% endif %} {%- endmacro -%} +{# Macro expects a backend or frontend object. #} +{% macro BasicAuthConfig(proxy) -%} +{% if proxy is defined %} +{# # basic auth expects a HTTP frontend/backend #} +{% if (proxy.mode == 'http' and proxy.basicAuthEnabled == '1') %} +{# # use the frontend/backend ID to identify the autogenerated userlist #} + acl auth_ok http_auth(list_{{proxy.id}}) + http-request auth if !auth_ok +{% endif %} +{% else %} +# ERROR: BasicAuthConfig called with empty data +{% endif %} +{%- endmacro -%} + +{# Macro expects a backend or frontend object. #} +{% macro UserlistConfig(proxy) -%} +{% if proxy is defined %} +{# # basic auth expects a HTTP frontend/backend #} +{% if (proxy.mode == 'http' and proxy.basicAuthEnabled == '1') %} +{# # use the frontend/backend ID to identify the autogenerated userlist #} + acl auth_ok http_auth({{proxy.id}}) + http-request auth if !auth_ok +{% endif %} +{% else %} +# ERROR: BasicAuthConfig called with empty data +{% endif %} +{%- endmacro -%} + +{# Macro expects a CSV list of users and validates them. #} +{% macro UserlistAddUsers(linkedUserData,linkedGroupData) -%} +{# # remember all users to avoid duplicate entries #} +{% set users_seen = [] %} +{# # process all users #} +{% if linkedUserData is defined %} +{% for user in linkedUserData.split(",") %} +{% set user_data = helpers.getUUID(user) %} +{# # check if this user can (still) be found in configuration #} +{% if user_data == {} %} +# ERROR: user data not found ({{user}}) +{% else %} +{% if user_data.name in users_seen %} + # WARNING: skipping duplicate username ({{user_data.name}}) +{% else %} +{% do users_seen.append(user_data.name) %} +{# # check if using an encrypted password #} +{% if user_data.password|default("")|truncate(1, False, '', 0) == '$' %} +{% set user_pwsec = 'password' %} +{% else %} +{% set user_pwsec = 'insecure-password' %} +{% endif %} + user {{user_data.name}} {{user_pwsec}} {{user_data.password}} +{% endif %} +{% endif %} +{% endfor %} +{% else %} +# ERROR: UserlistAddUsers called with empty user data +{% endif %} +{# # process all group members #} +{% if linkedGroupData is defined %} +{% for group in linkedGroupData.split(",") %} +{% set group_data = helpers.getUUID(group) %} +{# # check if this group can (still) be found in configuration #} +{% if group_data == {} %} +# ERROR: group data not found ({{group}}) +{% else %} +{# # extract user list from group object #} +{% for user in group_data.members.split(",") %} +{% set user_data = helpers.getUUID(user) %} +{% if user_data.name in users_seen %} + # WARNING: skipping duplicate username ({{user_data.name}}) +{% else %} +{% do users_seen.append(user_data.name) %} +{# # check if using an encrypted password #} +{% if user_data.password|default("")|truncate(1, False, '', 0) == '$' %} +{% set user_pwsec = 'password' %} +{% else %} +{% set user_pwsec = 'insecure-password' %} +{% endif %} + user {{user_data.name}} {{user_pwsec}} {{user_data.password}} +{% endif %} +{% endfor %} +{% endif %} +{% endfor %} +{% else %} +# ERROR: UserlistAddUsers called with empty group data +{% endif %} +{%- endmacro %} + +{# Macro expects a backend or frontend object. #} +{% macro AddUserlist(proxy) -%} +{% if proxy is defined %} +{% if (proxy.enabled|default("") == '1' and proxy.mode|default("") == 'http' and proxy.basicAuthEnabled|default("") == '1') %} +{# # call macro to generate list of unique users #} +{% set userlist_result = UserlistAddUsers(proxy.basicAuthUsers,proxy.basicAuthGroups) %} +{# # check result, skip when empty #} +{% if (userlist_result is defined and userlist_result|default("") != "" )%} +userlist list_{{proxy.id}} + # Origin: {{proxy.name}} +{{userlist_result}} +{% endif %} +{% endif %} +{% endif %} +{%- endmacro -%} + {% if not (helpers.exists('OPNsense.HAProxy.general') and OPNsense.HAProxy.general.enabled|default("0") == "1") %} # # NOTE: HAProxy is currently DISABLED @@ -747,6 +851,64 @@ defaults {% endif %} {% endif %} +{# ############################### #} +{# USERLISTS #} +{# ############################### #} +{# # NOTE: Yes, this config block is redundant and duplicates entries (on purpose). #} +{# # This makes it much easier for a user to compose this from the GUI. #} + +# autogenerated entries for all users/groups (may be used in ACLs) +{# # add an entry for every user #} +{% if helpers.exists('OPNsense.HAProxy.users') %} +{% for user_data in helpers.toList('OPNsense.HAProxy.users.user') %} +{% if user_data.enabled|default("") == '1' %} +userlist user_{{user_data.id}} +{# # check if using an encrypted password #} +{% if user_data.password|default("")|truncate(1, False, '', 0) == '$' %} +{% set user_pwsec = 'password' %} +{% else %} +{% set user_pwsec = 'insecure-password' %} +{% endif %} + user {{user_data.name}} {{user_pwsec}} {{user_data.password}} +{% endif %} +{% endfor %} +{% endif %} +{# # add an entry for every group #} +{% if helpers.exists('OPNsense.HAProxy.groups') %} +{% for group_data in helpers.toList('OPNsense.HAProxy.groups.group') %} +{% if group_data.enabled|default("") == '1' %} +userlist group_{{group_data.id}} +{# # extract user list from group object #} +{% for user in group_data.members.split(",") %} +{% set user_data = helpers.getUUID(user) %} +{# # check if using an encrypted password #} +{% if user_data.password|default("")|truncate(1, False, '', 0) == '$' %} +{% set user_pwsec = 'password' %} +{% else %} +{% set user_pwsec = 'insecure-password' %} +{% endif %} + user {{user_data.name}} {{user_pwsec}} {{user_data.password}} +{% endfor %} +{% endif %} +{% endfor %} +{% endif %} + +# autogenerated entries for config in backends/frontends +{% if helpers.exists('OPNsense.HAProxy.frontends') %} +{% for frontend in helpers.toList('OPNsense.HAProxy.frontends.frontend') %} +{# # call macro to generate userlist #} +{{ AddUserlist(frontend) -}} +{% endfor %} +{% endif %} +{% if helpers.exists('OPNsense.HAProxy.backends') %} +{% for backend in helpers.toList('OPNsense.HAProxy.backends.backend') %} +{# # call macro to generate userlist #} +{{ AddUserlist(backend) -}} +{% endfor %} +{% endif %} + +# XXX: autogenerated entries for stats + {# ############################### #} {# FRONTENDS #} {# ############################### #} @@ -875,6 +1037,8 @@ frontend {{frontend.name}} {% if frontend.logging_socketStats=='1' %} option socket-stats {% endif %} +{# # call macro to evaluate basic auth config #} +{{ BasicAuthConfig(frontend) -}} {# # action and ACL configuration #} {% if frontend.linkedActions|default("") != "" -%} {# # call macro to evaluate ACLs and actions #} @@ -1033,6 +1197,8 @@ backend {{backend.name}} {% if backend.tuning_retries|default("") != "" %} retries {{backend.tuning_retries}} {% endif %} +{# # call macro to evaluate basic auth config #} +{{ BasicAuthConfig(backend) -}} {# # action and ACL configuration #} {% if backend.linkedActions|default("") != "" -%} {# # call macro to evaluate ACLs and actions #} From 5baf2577724daef2d6fa24b4fc9abe12cfded917 Mon Sep 17 00:00:00 2001 From: Frank Wall Date: Sun, 11 Nov 2018 16:14:33 +0100 Subject: [PATCH 3/5] net/haproxy: use new user management for stats auth --- .../OPNsense/HAProxy/forms/main.xml | 15 +++-- .../app/models/OPNsense/HAProxy/HAProxy.xml | 24 ++++++++ .../OPNsense/HAProxy/Migrations/M2_6_0.php | 56 +++++++++++++++++++ .../templates/OPNsense/HAProxy/haproxy.conf | 27 ++++++--- 4 files changed, 108 insertions(+), 14 deletions(-) create mode 100644 net/haproxy/src/opnsense/mvc/app/models/OPNsense/HAProxy/Migrations/M2_6_0.php diff --git a/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/main.xml b/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/main.xml index ef67afc0f..417f1e51b 100644 --- a/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/main.xml +++ b/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/main.xml @@ -292,13 +292,18 @@ checkbox
    - haproxy.general.stats.users - + haproxy.general.stats.allowedUsers + select_multiple - true - - Enter user:password here. Finish with TAB. + Type username or choose from list. + + + haproxy.general.stats.allowedGroups + + select_multiple + true + Type group or choose from list. haproxy.general.stats.customOptions diff --git a/net/haproxy/src/opnsense/mvc/app/models/OPNsense/HAProxy/HAProxy.xml b/net/haproxy/src/opnsense/mvc/app/models/OPNsense/HAProxy/HAProxy.xml index 72bd1637f..66b025dfe 100644 --- a/net/haproxy/src/opnsense/mvc/app/models/OPNsense/HAProxy/HAProxy.xml +++ b/net/haproxy/src/opnsense/mvc/app/models/OPNsense/HAProxy/HAProxy.xml @@ -289,6 +289,30 @@ /^((([0-9a-zA-Z._\-]+:[0-9a-zA-Z._\-]+)([,]){0,1}))*/u Please provide a valid user and password, i.e. user:secret123. + + + + + Related user not found + Y + N + + + + + + Related group not found + Y + N + N diff --git a/net/haproxy/src/opnsense/mvc/app/models/OPNsense/HAProxy/Migrations/M2_6_0.php b/net/haproxy/src/opnsense/mvc/app/models/OPNsense/HAProxy/Migrations/M2_6_0.php new file mode 100644 index 000000000..53bd4a432 --- /dev/null +++ b/net/haproxy/src/opnsense/mvc/app/models/OPNsense/HAProxy/Migrations/M2_6_0.php @@ -0,0 +1,56 @@ +general->stats->users)) { + + // Add new user for each entry + $UUIDlist = array(); + foreach (explode(',', (string)$model->general->stats->users) as $statsuser) { + $olddata = explode(':',$statsuser,2); + $userNode = $model->users->user->Add(); + $userNode->name = (string)$olddata[0]; + $userNode->description = 'stats user'; + $userNode->password = (string)$olddata[1]; + $userNode->enabled = 1; + $UUIDlist[] = $userNode->getAttributes()['uuid']; + } + + // Add collected UUIDs to new list of allowed users + $model->general->stats->allowedUsers = (string)implode(',', $UUIDlist); + } + } +} diff --git a/net/haproxy/src/opnsense/service/templates/OPNsense/HAProxy/haproxy.conf b/net/haproxy/src/opnsense/service/templates/OPNsense/HAProxy/haproxy.conf index 784d4d06b..a1bcbbaba 100644 --- a/net/haproxy/src/opnsense/service/templates/OPNsense/HAProxy/haproxy.conf +++ b/net/haproxy/src/opnsense/service/templates/OPNsense/HAProxy/haproxy.conf @@ -907,7 +907,18 @@ userlist group_{{group_data.id}} {% endfor %} {% endif %} -# XXX: autogenerated entries for stats +# autogenerated entries for stats +{% if OPNsense.HAProxy.general.stats.remoteEnabled|default("") == "1" %} +{% if (OPNsense.HAProxy.general.stats.allowedUsers|default("") != "") or (OPNsense.HAProxy.general.stats.allowedGroups|default("") != "") %} +{# # call macro to generate list of unique users #} +{% set userlist_result = UserlistAddUsers(OPNsense.HAProxy.general.stats.allowedUsers, OPNsense.HAProxy.general.stats.allowedGroups) %} +{# # check result, skip when empty #} +{% if (userlist_result is defined and userlist_result|default("") != "" )%} +userlist stats_auth +{{userlist_result}} +{% endif %} +{% endif %} +{% endif %} {# ############################### #} {# FRONTENDS #} @@ -1382,15 +1393,12 @@ listen remote_statistics {% endfor %} mode http stats uri /haproxy?stats - stats realm HAProxy\ statistics stats hide-version {# # enable authentication? #} {% if OPNsense.HAProxy.general.stats.authEnabled|default("") == "1" %} -{% if OPNsense.HAProxy.general.stats.users|default("") != "" %} -{% for statsuser in OPNsense.HAProxy.general.stats.users.split(",") %} - stats auth {{statsuser}} -{% endfor %} -{% endif %} + acl auth_ok http_auth(stats_auth) + stats http-request allow if auth_ok + stats http-request auth realm HAProxy\ statistics {% endif %} {% if OPNsense.HAProxy.general.stats.customOptions|default("") != "" %} # WARNING: pass through options below this line @@ -1401,9 +1409,10 @@ listen remote_statistics {% else %} # ERROR: remote statistics disabled, because no listen address was specified {% endif %} -{% endif %} -{% else %} +{% else %} # statistics are DISABLED +{% endif %} + {% endif %} {% endif %} From 1e949192b431802d71a2307654b36163fa8543f5 Mon Sep 17 00:00:00 2001 From: Frank Wall Date: Sun, 11 Nov 2018 18:26:46 +0100 Subject: [PATCH 4/5] net/haproxy: support HTTP Basic Auth in ACLs --- .../OPNsense/HAProxy/forms/dialogAcl.xml | 17 ++++++ .../app/models/OPNsense/HAProxy/HAProxy.xml | 25 ++++++++ .../templates/OPNsense/HAProxy/haproxy.conf | 57 +++++++------------ 3 files changed, 64 insertions(+), 35 deletions(-) diff --git a/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/dialogAcl.xml b/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/dialogAcl.xml index 886d9f206..f56065c94 100644 --- a/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/dialogAcl.xml +++ b/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/dialogAcl.xml @@ -467,6 +467,23 @@ text + + + header + + + + acl.allowedUsers + + select_multiple + true + + + acl.allowedGroups + + select_multiple + true + header diff --git a/net/haproxy/src/opnsense/mvc/app/models/OPNsense/HAProxy/HAProxy.xml b/net/haproxy/src/opnsense/mvc/app/models/OPNsense/HAProxy/HAProxy.xml index 66b025dfe..6b135564a 100644 --- a/net/haproxy/src/opnsense/mvc/app/models/OPNsense/HAProxy/HAProxy.xml +++ b/net/haproxy/src/opnsense/mvc/app/models/OPNsense/HAProxy/HAProxy.xml @@ -1246,6 +1246,7 @@ Y + HTTP Basic Auth: username/password from client matches selected User/Group Host starts with Host ends with Host matches @@ -1631,6 +1632,30 @@ N N + + + + + Related user not found + Y + N + + + + + + Related group not found + Y + N + diff --git a/net/haproxy/src/opnsense/service/templates/OPNsense/HAProxy/haproxy.conf b/net/haproxy/src/opnsense/service/templates/OPNsense/HAProxy/haproxy.conf index a1bcbbaba..398e9561b 100644 --- a/net/haproxy/src/opnsense/service/templates/OPNsense/HAProxy/haproxy.conf +++ b/net/haproxy/src/opnsense/service/templates/OPNsense/HAProxy/haproxy.conf @@ -72,7 +72,14 @@ {% endif %} {% do acls_seen.append(acl_data.id) %} {% set acl_options = [] %} -{% if acl_data.expression == 'hdr_beg' %} +{% if acl_data.expression == 'http_auth' %} +{% if acl_data.allowedUsers|default("") != "" or acl_data.allowedGroups|default("") != "" %} +{% do acl_options.append('http_auth(acl_' ~ acl_data.id ~ ')') %} +{% else %} +{% set acl_enabled = '0' %} + # ERROR: missing parameters +{% endif %} +{% elif acl_data.expression == 'hdr_beg' %} {% if acl_data.hdr_beg|default("") != "" %} {% do acl_options.append('hdr_beg(host) -i ' ~ acl_data.hdr_beg) %} {% else %} @@ -678,7 +685,7 @@ {% endif %} {% endfor %} {% else %} -# ERROR: UserlistAddUsers called with empty user data + # WARNING: UserlistAddUsers called with empty user data {% endif %} {# # process all group members #} {% if linkedGroupData is defined %} @@ -686,7 +693,7 @@ {% set group_data = helpers.getUUID(group) %} {# # check if this group can (still) be found in configuration #} {% if group_data == {} %} -# ERROR: group data not found ({{group}}) + # WARNING: group data not found ({{group}}) {% else %} {# # extract user list from group object #} {% for user in group_data.members.split(",") %} @@ -707,7 +714,7 @@ {% endif %} {% endfor %} {% else %} -# ERROR: UserlistAddUsers called with empty group data + # WARNING: UserlistAddUsers called with empty group data {% endif %} {%- endmacro %} @@ -857,38 +864,18 @@ defaults {# # NOTE: Yes, this config block is redundant and duplicates entries (on purpose). #} {# # This makes it much easier for a user to compose this from the GUI. #} -# autogenerated entries for all users/groups (may be used in ACLs) -{# # add an entry for every user #} -{% if helpers.exists('OPNsense.HAProxy.users') %} -{% for user_data in helpers.toList('OPNsense.HAProxy.users.user') %} -{% if user_data.enabled|default("") == '1' %} -userlist user_{{user_data.id}} -{# # check if using an encrypted password #} -{% if user_data.password|default("")|truncate(1, False, '', 0) == '$' %} -{% set user_pwsec = 'password' %} -{% else %} -{% set user_pwsec = 'insecure-password' %} +# autogenerated entries for ACLs +{% if helpers.exists('OPNsense.HAProxy.acls') %} +{% for acl in helpers.toList('OPNsense.HAProxy.acls.acl') %} +{% if (acl.allowedUsers|default("") != "") or (acl.allowedGroups|default("") != "") %} +{# # call macro to generate list of unique users #} +{% set userlist_result = UserlistAddUsers(acl.allowedUsers, acl.allowedGroups) %} +{# # check result, skip when empty #} +{% if (userlist_result is defined and userlist_result|default("") != "" )%} +userlist acl_{{acl.id}} + # Origin: {{acl.name}} +{{userlist_result}} {% endif %} - user {{user_data.name}} {{user_pwsec}} {{user_data.password}} -{% endif %} -{% endfor %} -{% endif %} -{# # add an entry for every group #} -{% if helpers.exists('OPNsense.HAProxy.groups') %} -{% for group_data in helpers.toList('OPNsense.HAProxy.groups.group') %} -{% if group_data.enabled|default("") == '1' %} -userlist group_{{group_data.id}} -{# # extract user list from group object #} -{% for user in group_data.members.split(",") %} -{% set user_data = helpers.getUUID(user) %} -{# # check if using an encrypted password #} -{% if user_data.password|default("")|truncate(1, False, '', 0) == '$' %} -{% set user_pwsec = 'password' %} -{% else %} -{% set user_pwsec = 'insecure-password' %} -{% endif %} - user {{user_data.name}} {{user_pwsec}} {{user_data.password}} -{% endfor %} {% endif %} {% endfor %} {% endif %} From 9f312bf1e58efc64ed2800ffc9a6d2aa1df22fc1 Mon Sep 17 00:00:00 2001 From: Frank Wall Date: Sun, 11 Nov 2018 18:30:51 +0100 Subject: [PATCH 5/5] net/haproxy: fixes for #970 --- .../app/controllers/OPNsense/HAProxy/forms/dialogBackend.xml | 2 +- .../mvc/app/controllers/OPNsense/HAProxy/forms/dialogUser.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/dialogBackend.xml b/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/dialogBackend.xml index 744615c4a..e00da8571 100644 --- a/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/dialogBackend.xml +++ b/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/dialogBackend.xml @@ -232,7 +232,7 @@ backend.basicAuthEnabled checkbox - + Enable HTTP Basic Authentication. backend.basicAuthUsers diff --git a/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/dialogUser.xml b/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/dialogUser.xml index 000bd9a65..7644058ee 100644 --- a/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/dialogUser.xml +++ b/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/dialogUser.xml @@ -21,6 +21,6 @@ user.password password -
    NOTE: Avoid using unencrypted passwords that start with a $-sign, because this indicates an encrypted password and will make it impossible to authenticate.
    ]]>
    +
    NOTE: Avoid using unencrypted passwords that start with a $-sign, because this indicates an encrypted password and will make it impossible to authenticate.
    ]]>