Merge pull request #970 from fraenki/haproxy_210_2

net/haproxy: more features for release 2.10
This commit is contained in:
Frank Wall
2018-11-11 19:05:35 +01:00
committed by GitHub
13 changed files with 913 additions and 15 deletions
@@ -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"
);
}
}
@@ -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");
@@ -467,6 +467,23 @@
<type>text</type>
<help><![CDATA[The value of the Server Name TLS extension sent by a client ends with the specified string (suffix match).]]></help>
</field>
<field>
<label>Parameters</label>
<type>header</type>
<style>expression_table table_http_auth</style>
</field>
<field>
<id>acl.allowedUsers</id>
<label>Allowed Users</label>
<type>select_multiple</type>
<allownew>true</allownew>
</field>
<field>
<id>acl.allowedGroups</id>
<label>Allowed Groups</label>
<type>select_multiple</type>
<allownew>true</allownew>
</field>
<field>
<label>Parameters</label>
<type>header</type>
@@ -223,6 +223,31 @@
<help><![CDATA[The length of the period over which the average is measured. It reports the average outgoing bytes rate over that period, in bytes per period. Defaults to milliseconds. Optionally the unit may be specified as either "d", "h", "m", "s", "ms" or "us".]]></help>
<advanced>true</advanced>
</field>
<field>
<label>Basic Authentication</label>
<type>header</type>
<style>mode_table table_http</style>
</field>
<field>
<id>backend.basicAuthEnabled</id>
<label>Enable</label>
<type>checkbox</type>
<help>Enable HTTP Basic Authentication.</help>
</field>
<field>
<id>backend.basicAuthUsers</id>
<label>Allowed Users</label>
<type>select_multiple</type>
<allownew>true</allownew>
<hint>Type username or choose from list.</hint>
</field>
<field>
<id>backend.basicAuthGroups</id>
<label>Allowed Groups</label>
<type>select_multiple</type>
<allownew>true</allownew>
<hint>Type group or choose from list.</hint>
</field>
<field>
<label>Tuning Options</label>
<type>header</type>
@@ -161,6 +161,31 @@
<help><![CDATA[Select CRLs to use for client certificate authentication. <br/>To import additional CRLs, go to <a href="/system_crlmanager.php">CRL Manager</a>.]]></help>
<hint>Type CRL name or choose from list.</hint>
</field>
<field>
<label>Basic Authentication</label>
<type>header</type>
<style>mode_table table_http</style>
</field>
<field>
<id>frontend.basicAuthEnabled</id>
<label>Enable</label>
<type>checkbox</type>
<help><![CDATA[Enable HTTP Basic Authentication.]]></help>
</field>
<field>
<id>frontend.basicAuthUsers</id>
<label>Allowed Users</label>
<type>select_multiple</type>
<allownew>true</allownew>
<hint>Type username or choose from list.</hint>
</field>
<field>
<id>frontend.basicAuthGroups</id>
<label>Allowed Groups</label>
<type>select_multiple</type>
<allownew>true</allownew>
<hint>Type group or choose from list.</hint>
</field>
<field>
<label>Tuning Options</label>
<type>header</type>
@@ -0,0 +1,27 @@
<form>
<field>
<id>group.enabled</id>
<label>Enabled</label>
<type>checkbox</type>
<help>Enable this group.</help>
</field>
<field>
<id>group.name</id>
<label>Name</label>
<type>text</type>
<help>Name to identify this group.</help>
</field>
<field>
<id>group.description</id>
<label>Description</label>
<type>text</type>
<help>Description for this group.</help>
</field>
<field>
<id>group.members</id>
<label>Members</label>
<type>select_multiple</type>
<allownew>true</allownew>
<hint>Type username or choose from list.</hint>
</field>
</form>
@@ -0,0 +1,26 @@
<form>
<field>
<id>user.enabled</id>
<label>Enabled</label>
<type>checkbox</type>
<help>Enable this user.</help>
</field>
<field>
<id>user.name</id>
<label>Name</label>
<type>text</type>
<help>Name to identify this user.</help>
</field>
<field>
<id>user.description</id>
<label>Description</label>
<type>text</type>
<help>Description for this user.</help>
</field>
<field>
<id>user.password</id>
<label>Password</label>
<type>password</type>
<help><![CDATA[Both encrypted and unencrypted passwords can be used. Most systems support MD5, SHA-256, SHA-512, and, of course, the classic DES-based method of encrypting passwords.<br/><div class="text-info"><b>NOTE:</b> Avoid using unencrypted passwords that start with a $-sign, because this indicates an encrypted password and will make it impossible to authenticate.</div>]]></help>
</field>
</form>
@@ -292,13 +292,18 @@
<type>checkbox</type>
</field>
<field>
<id>haproxy.general.stats.users</id>
<label>Stats users</label>
<id>haproxy.general.stats.allowedUsers</id>
<label>Allowed Users</label>
<type>select_multiple</type>
<style>tokenize</style>
<allownew>true</allownew>
<help><![CDATA[Grant access to HAProxy statistics page. Please provide both user and password in clear text separated by a ':', i.e. john:secret123 or jdoe:anonymous. Use TAB key to complete adding a user.]]></help>
<hint>Enter user:password here. Finish with TAB.</hint>
<hint>Type username or choose from list.</hint>
</field>
<field>
<id>haproxy.general.stats.allowedGroups</id>
<label>Allowed Groups</label>
<type>select_multiple</type>
<allownew>true</allownew>
<hint>Type group or choose from list.</hint>
</field>
<field>
<id>haproxy.general.stats.customOptions</id>
@@ -1,6 +1,6 @@
<model>
<mount>//OPNsense/HAProxy</mount>
<version>2.5.0</version>
<version>2.6.0</version>
<description>the HAProxy load balancer</description>
<items>
<general>
@@ -289,6 +289,30 @@
<mask>/^((([0-9a-zA-Z._\-]+:[0-9a-zA-Z._\-]+)([,]){0,1}))*/u</mask>
<ValidationMessage>Please provide a valid user and password, i.e. user:secret123.</ValidationMessage>
</users>
<allowedUsers type="ModelRelationField">
<Model>
<template>
<source>OPNsense.HAProxy.HAProxy</source>
<items>users.user</items>
<display>name</display>
</template>
</Model>
<ValidationMessage>Related user not found</ValidationMessage>
<multiple>Y</multiple>
<Required>N</Required>
</allowedUsers>
<allowedGroups type="ModelRelationField">
<Model>
<template>
<source>OPNsense.HAProxy.HAProxy</source>
<items>groups.group</items>
<display>name</display>
</template>
</Model>
<ValidationMessage>Related group not found</ValidationMessage>
<multiple>Y</multiple>
<Required>N</Required>
</allowedGroups>
<customOptions type="TextField">
<Required>N</Required>
</customOptions>
@@ -429,6 +453,34 @@
<Multiple>Y</Multiple>
<ValidationMessage>Please select a valid CA from the list.</ValidationMessage>
</ssl_clientAuthCRLs>
<basicAuthEnabled type="BooleanField">
<default>0</default>
<Required>N</Required>
</basicAuthEnabled>
<basicAuthUsers type="ModelRelationField">
<Model>
<template>
<source>OPNsense.HAProxy.HAProxy</source>
<items>users.user</items>
<display>name</display>
</template>
</Model>
<ValidationMessage>Related user not found</ValidationMessage>
<multiple>Y</multiple>
<Required>N</Required>
</basicAuthUsers>
<basicAuthGroups type="ModelRelationField">
<Model>
<template>
<source>OPNsense.HAProxy.HAProxy</source>
<items>groups.group</items>
<display>name</display>
</template>
</Model>
<ValidationMessage>Related group not found</ValidationMessage>
<multiple>Y</multiple>
<Required>N</Required>
</basicAuthGroups>
<tuning_maxConnections type="IntegerField">
<MinimumValue>1</MinimumValue>
<MaximumValue>500000</MaximumValue>
@@ -830,6 +882,34 @@
<ValidationMessage>Should be a number between 1 and 8 characters, optionally followed by either "d", "h", "m", "s", "ms" or "us".</ValidationMessage>
<Required>N</Required>
</stickiness_bytesOutRatePeriod>
<basicAuthEnabled type="BooleanField">
<default>0</default>
<Required>N</Required>
</basicAuthEnabled>
<basicAuthUsers type="ModelRelationField">
<Model>
<template>
<source>OPNsense.HAProxy.HAProxy</source>
<items>users.user</items>
<display>name</display>
</template>
</Model>
<ValidationMessage>Related user not found</ValidationMessage>
<multiple>Y</multiple>
<Required>N</Required>
</basicAuthUsers>
<basicAuthGroups type="ModelRelationField">
<Model>
<template>
<source>OPNsense.HAProxy.HAProxy</source>
<items>groups.group</items>
<display>name</display>
</template>
</Model>
<ValidationMessage>Related group not found</ValidationMessage>
<multiple>Y</multiple>
<Required>N</Required>
</basicAuthGroups>
<tuning_timeoutConnect type="TextField">
<mask>/^([0-9]{1,8}(?:us|ms|s|m|h|d)?)/u</mask>
<ValidationMessage>Should be a number between 1 and 8 characters, optionally followed by either "d", "h", "m", "s", "ms" or "us".</ValidationMessage>
@@ -1166,6 +1246,7 @@
<expression type="OptionField">
<Required>Y</Required>
<OptionValues>
<http_auth>HTTP Basic Auth: username/password from client matches selected User/Group</http_auth>
<hdr_beg>Host starts with</hdr_beg>
<hdr_end>Host ends with</hdr_end>
<hdr>Host matches</hdr>
@@ -1551,6 +1632,30 @@
<Required>N</Required>
<Required>N</Required>
</queryBackend>
<allowedUsers type="ModelRelationField">
<Model>
<template>
<source>OPNsense.HAProxy.HAProxy</source>
<items>users.user</items>
<display>name</display>
</template>
</Model>
<ValidationMessage>Related user not found</ValidationMessage>
<multiple>Y</multiple>
<Required>N</Required>
</allowedUsers>
<allowedGroups type="ModelRelationField">
<Model>
<template>
<source>OPNsense.HAProxy.HAProxy</source>
<items>groups.group</items>
<display>name</display>
</template>
</Model>
<ValidationMessage>Related group not found</ValidationMessage>
<multiple>Y</multiple>
<Required>N</Required>
</allowedGroups>
</acl>
</acls>
<actions>
@@ -1921,5 +2026,64 @@
</content>
</mapfile>
</mapfiles>
<groups>
<group type="ArrayField">
<id type="UniqueIdField">
<Required>Y</Required>
</id>
<enabled type="BooleanField">
<default>1</default>
<Required>Y</Required>
</enabled>
<name type="TextField">
<mask>/^[^\t^,^;^\.^\[^\]^\{^\}]{1,255}$/u</mask>
<ValidationMessage>Should be a string between 1 and 255 characters.</ValidationMessage>
<Required>Y</Required>
</name>
<description type="TextField">
<mask>/^.{1,255}$/u</mask>
<ValidationMessage>Should be a string between 1 and 255 characters.</ValidationMessage>
<Required>N</Required>
</description>
<members type="ModelRelationField">
<Model>
<template>
<source>OPNsense.HAProxy.HAProxy</source>
<items>users.user</items>
<display>name</display>
</template>
</Model>
<ValidationMessage>Related user not found</ValidationMessage>
<Multiple>Y</Multiple>
<Required>N</Required>
</members>
</group>
</groups>
<users>
<user type="ArrayField">
<id type="UniqueIdField">
<Required>Y</Required>
</id>
<enabled type="BooleanField">
<default>1</default>
<Required>Y</Required>
</enabled>
<name type="TextField">
<mask>/^[^\t^,^;^\.^\[^\]^\{^\}]{1,255}$/u</mask>
<ValidationMessage>Should be a string between 1 and 255 characters.</ValidationMessage>
<Required>Y</Required>
</name>
<description type="TextField">
<mask>/^.{1,255}$/u</mask>
<ValidationMessage>Should be a string between 1 and 255 characters.</ValidationMessage>
<Required>N</Required>
</description>
<password type="TextField">
<mask>/^.{1,512}$/u</mask>
<ValidationMessage>Should be a string between 1 and 512 characters.</ValidationMessage>
<Required>Y</Required>
</password>
</user>
</users>
</items>
</model>
@@ -14,6 +14,8 @@
<HealthChecks VisibleName="Health Checks" url="/ui/haproxy#healthchecks"/>
<Actions VisibleName="Actions" url="/ui/haproxy#actions"/>
<Acls VisibleName="ACLs" url="/ui/haproxy#acls"/>
<Users VisibleName="Users" url="/ui/haproxy#users"/>
<Groups VisibleName="Groups" url="/ui/haproxy#groups"/>
<Luas VisibleName="Lua Scripts" url="/ui/haproxy#luas"/>
<Errorfiles VisibleName="Error Files" url="/ui/haproxy#errorfiles"/>
<Mapfiles VisibleName="Map Files" url="/ui/haproxy#mapfiles"/>
@@ -0,0 +1,56 @@
<?php
/**
* Copyright (C) 2018 Frank Wall
*
* 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\HAProxy\Migrations;
use OPNsense\Base\BaseModelMigration;
class M2_6_0 extends BaseModelMigration
{
public function run($model)
{
// Migrate old stats user:password entries to new user management feature
if (!empty((string)$model->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);
}
}
}
@@ -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.
</ul>
</li>
<li role="presentation" class="dropdown">
<a data-toggle="dropdown" href="#" class="dropdown-toggle pull-right visible-lg-inline-block visible-md-inline-block visible-xs-inline-block visible-sm-inline-block" role="button">
<b><span class="caret"></span></b>
</a>
<a data-toggle="tab" onclick="$('#{% if showIntro|default('0')=='1' %}user-management-introduction{% else %}users-tab{% endif %}').click();" class="visible-lg-inline-block visible-md-inline-block visible-xs-inline-block visible-sm-inline-block" style="border-right:0px;"><b>{{ lang._('User Management') }}</b></a>
<ul class="dropdown-menu" role="menu">
{% if showIntro|default('0')=='1' %}
<li><a data-toggle="tab" id="user-management-introduction" href="#subtab_haproxy-user-management-introduction">{{ lang._('Introduction') }}</a></li>
{% endif %}
<li><a data-toggle="tab" id="users-tab" href="#users">{{ lang._('Users') }}</a></li>
<li><a data-toggle="tab" href="#groups">{{ lang._('Groups') }}</a></li>
</ul>
</li>
{# add automatically generated tabs #}
{% for tab in mainForm['tabs']|default([]) %}
{% if tab['subtabs']|default(false) %}
@@ -541,6 +581,20 @@ POSSIBILITY OF SUCH DAMAGE.
</div>
</div>
<div id="subtab_haproxy-user-management-introduction" class="tab-pane fade">
<div class="col-md-12">
<h1>{{ lang._('User Management') }}</h1>
<p>{{ 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.") }}</p>
<ul>
<li>{{ lang._('%sUser:%s A username/password combination. Both secure (encrypted) and insecure (unencrypted) passwords can be used.') | format('<b>', '</b>') }}</li>
<li>{{ 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('<b>', '</b>') }}</li>
</ul>
<p>{{ 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.') }}</p>
<p>{{ lang._("For more information on HAProxy's %suser/group management%s see the %sofficial documentation%s.") | format('<b>', '</b>', '<a href="http://cbonte.github.io/haproxy-dconv/1.8/configuration.html#3.4" target="_blank">', '</a>') }}</p>
<br/>
</div>
</div>
<div id="subtab_haproxy-advanced-introduction" class="tab-pane fade">
<div class="col-md-12">
<h1>{{ lang._('Advanced Features') }}</h1>
@@ -780,6 +834,76 @@ POSSIBILITY OF SUCH DAMAGE.
</div>
</div>
<div id="users" class="tab-pane fade">
<!-- tab page "users" -->
<table id="grid-users" class="table table-condensed table-hover table-striped table-responsive" data-editDialog="DialogUser">
<thead>
<tr>
<th data-column-id="enabled" data-width="6em" data-type="string" data-formatter="rowtoggle">{{ lang._('Enabled') }}</th>
<th data-column-id="userid" data-type="number" data-visible="false">{{ lang._('User ID') }}</th>
<th data-column-id="name" data-type="string">{{ lang._('Username') }}</th>
<th data-column-id="description" data-type="string">{{ lang._('Description') }}</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>
<!-- apply button -->
<div class="col-md-12">
<hr/>
<button class="btn btn-primary" id="reconfigureAct-users" type="button"><b>{{ lang._('Apply') }}</b><i id="reconfigureAct_progress" class=""></i></button>
<button class="btn btn-primary" id="configtestAct-users" type="button"><b>{{ lang._('Test syntax') }}</b><i id="configtestAct_progress" class=""></i></button>
<br/>
<br/>
</div>
</div>
<div id="groups" class="tab-pane fade">
<!-- tab page "groups" -->
<table id="grid-groups" class="table table-condensed table-hover table-striped table-responsive" data-editDialog="DialogGroup">
<thead>
<tr>
<th data-column-id="enabled" data-width="6em" data-type="string" data-formatter="rowtoggle">{{ lang._('Enabled') }}</th>
<th data-column-id="groupid" data-type="number" data-visible="false">{{ lang._('Group ID') }}</th>
<th data-column-id="name" data-type="string">{{ lang._('Group') }}</th>
<th data-column-id="description" data-type="string">{{ lang._('Description') }}</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>
<!-- apply button -->
<div class="col-md-12">
<hr/>
<button class="btn btn-primary" id="reconfigureAct-groups" type="button"><b>{{ lang._('Apply') }}</b><i id="reconfigureAct_progress" class=""></i></button>
<button class="btn btn-primary" id="configtestAct-groups" type="button"><b>{{ lang._('Test syntax') }}</b><i id="configtestAct_progress" class=""></i></button>
<br/>
<br/>
</div>
</div>
<div id="luas" class="tab-pane fade">
<!-- tab page "luas" -->
<table id="grid-luas" class="table table-condensed table-hover table-striped table-responsive" data-editDialog="DialogLua">
@@ -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')])}}
@@ -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 %}
@@ -623,6 +630,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 %}
# WARNING: 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 == {} %}
# WARNING: 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 %}
# WARNING: 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 +858,55 @@ 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 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 %}
{% 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 %}
# 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 #}
{# ############################### #}
@@ -875,6 +1035,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 +1195,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 #}
@@ -1216,15 +1380,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
@@ -1235,9 +1396,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 %}