net/haproxy: add user/group management, refs #300

This commit is contained in:
Frank Wall
2018-11-09 13:41:34 +01:00
parent a73e126d79
commit 9a0ca36dc3
7 changed files with 504 additions and 1 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");
@@ -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 secure (encrypted) and insecure (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>
@@ -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>
@@ -1921,5 +1921,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"/>
@@ -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.') }}</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')])}}