security/acme-client: add acme.sh to plugins, closes #6

This commit is contained in:
Frank Wall
2017-01-27 08:33:37 +01:00
committed by Franco Fichtner
parent 928228a860
commit dd4853d09f
47 changed files with 11147 additions and 0 deletions
+7
View File
@@ -0,0 +1,7 @@
PLUGIN_NAME= acme-client
PLUGIN_VERSION= 1.0
PLUGIN_COMMENT= Lets Encrypt client
#PLUGIN_DEPENDS= acme.sh
PLUGIN_MAINTAINER= opnsense@moov.de
.include "../../Mk/plugins.mk"
@@ -0,0 +1,95 @@
<?php
/**
* Copyright (C) 2017 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.
*
*/
function acmeclient_enabled()
{
global $config;
if (isset($config['OPNsense']['AcmeClient']['general']['enabled']) && $config['OPNsense']['AcmeClient']['general']['enabled'] == 1) {
return true;
}
return false;
}
function acmeclient_firewall($fw)
{
if (!acmeclient_enabled()) {
return;
}
// TODO
$fw->registerAnchor('acme-client/*', 'nat');
$fw->registerAnchor('acme-client/*', 'rdr');
$fw->registerAnchor('acme-client/*', 'fw');
}
/**
* register legacy service
* @return array
*/
function acmeclient_services()
{
if (!acmeclient_enabled()) {
return;
}
global $config;
$services = array();
$services[] = array(
'description' => gettext('Secure Lets Encrypt client'),
'configd' => array(
'restart' => array('acme-http-challenge restart'),
'start' => array('acme-http-challenge start'),
'stop' => array('acme-http-challenge stop'),
),
'name' => 'acmeclient',
);
return $services;
}
/**
* sync configuration via xmlrpc
* @return array
*/
/**
XXX: needs investigation, auto-renewal must be disabled on secondary node(s)
function acmeclient_xmlrpc_sync()
{
$result = array();
$result['id'] = 'acmeclient';
$result['section'] = 'OPNsense.acmeclient';
$result['description'] = gettext('Lets Encrypt Client');
return array($result);
}
*/
@@ -0,0 +1,46 @@
<?php
/**
* Copyright (C) 2017 Frank Wall
* Copyright (C) 2015 Deciso B.V.
*
* 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\AcmeClient;
/**
* Class AccountsController
* @package OPNsense\AcmeClient
*/
class AccountsController extends \OPNsense\Base\IndexController
{
public function indexAction()
{
$this->view->title = "Let's Encrypt Accounts";
// include form definitions
$this->view->formDialogAccount = $this->getForm("dialogAccount");
// choose template
$this->view->pick('OPNsense/AcmeClient/accounts');
}
}
@@ -0,0 +1,209 @@
<?php
/**
* Copyright (C) 2017 Frank Wall
* Copyright (C) 2015 Deciso B.V.
*
* 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\AcmeClient\Api;
use \OPNsense\Base\ApiControllerBase;
use \OPNsense\AcmeClient\AcmeClient;
use \OPNsense\Core\Config;
use \OPNsense\Base\UIModelGrid;
/**
* Class AccountsController
* @package OPNsense\AcmeClient
*/
class AccountsController extends ApiControllerBase
{
/**
* Validate and save model after update or insertion.
* Use the reference node and tag to rename validation output for a specific
* node to a new offset, which makes it easier to reference specific uuids
* without having to use them in the frontend descriptions.
* @param $mdl model reference
* @param $node reference node, to use as relative offset
* @param $reference reference for validation output, used to rename the validation output keys
* @return array result / validation output
*/
private function save($mdl, $node = null, $reference = null)
{
$result = array("result"=>"failed","validations" => array());
// perform validation
$valMsgs = $mdl->performValidation();
foreach ($valMsgs as $field => $msg) {
// replace absolute path to attribute for relative one at uuid.
if ($node != null) {
$fieldnm = str_replace($node->__reference, $reference, $msg->getField());
$result["validations"][$fieldnm] = $msg->getMessage();
} else {
$result["validations"][$msg->getField()] = $msg->getMessage();
}
}
// serialize model to config and save when there are no validation errors
if (count($result['validations']) == 0) {
// save config if validated correctly
$mdl->serializeToConfig();
Config::getInstance()->save();
$result = array("result" => "saved");
}
return $result;
}
/**
* retrieve account settings or return defaults
* @param $uuid item unique id
* @return array
*/
public function getAction($uuid = null)
{
$mdlAcme = new AcmeClient();
if ($uuid != null) {
$node = $mdlAcme->getNodeByReference('accounts.account.'.$uuid);
if ($node != null) {
// return node
return array("account" => $node->getNodes());
}
} else {
// generate new node, but don't save to disc
$node = $mdlAcme->accounts->account->add() ;
return array("account" => $node->getNodes());
}
return array();
}
/**
* update account with given properties
* @param $uuid item unique id
* @return array
*/
public function setAction($uuid)
{
if ($this->request->isPost() && $this->request->hasPost("account")) {
$mdlAcme = new AcmeClient();
if ($uuid != null) {
$node = $mdlAcme->getNodeByReference('accounts.account.'.$uuid);
if ($node != null) {
$node->setNodes($this->request->getPost("account"));
return $this->save($mdlAcme, $node, "account");
}
}
}
return array("result"=>"failed");
}
/**
* add new account and set with attributes from post
* @return array
*/
public function addAction()
{
$result = array("result"=>"failed");
if ($this->request->isPost() && $this->request->hasPost("account")) {
$mdlAcme = new AcmeClient();
$node = $mdlAcme->accounts->account->Add();
$node->setNodes($this->request->getPost("account"));
return $this->save($mdlAcme, $node, "account");
}
return $result;
}
/**
* delete account by uuid
* @param $uuid item unique id
* @return array status
*/
public function delAction($uuid)
{
$result = array("result"=>"failed");
if ($this->request->isPost()) {
$mdlAcme = new AcmeClient();
if ($uuid != null) {
if ($mdlAcme->accounts->account->del($uuid)) {
// if item is removed, serialize to config and save
$mdlAcme->serializeToConfig();
Config::getInstance()->save();
$result['result'] = 'deleted';
} else {
$result['result'] = 'not found';
}
}
}
return $result;
}
/**
* toggle account 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 toggleAction($uuid, $enabled = null)
{
$result = array("result" => "failed");
if ($this->request->isPost()) {
$mdlAcme = new AcmeClient();
if ($uuid != null) {
$node = $mdlAcme->getNodeByReference('accounts.account.' . $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
$mdlAcme->serializeToConfig();
Config::getInstance()->save();
}
}
}
return $result;
}
/**
* search accounts
* @return array
*/
public function searchAction()
{
$this->sessionClose();
$mdlAcme = new AcmeClient();
$grid = new UIModelGrid($mdlAcme->accounts->account);
return $grid->fetchBindRequest(
$this->request,
array("enabled", "name", "email","accountid"),
"name"
);
}
}
@@ -0,0 +1,259 @@
<?php
/**
* Copyright (C) 2017 Frank Wall
* Copyright (C) 2015 Deciso B.V.
*
* 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\AcmeClient\Api;
use \OPNsense\Base\ApiControllerBase;
use \OPNsense\AcmeClient\AcmeClient;
use \OPNsense\Core\Backend;
use \OPNsense\Core\Config;
use \OPNsense\Base\UIModelGrid;
/**
* Class CertificatesController
* @package OPNsense\AcmeClient
*/
class CertificatesController extends ApiControllerBase
{
/**
* Validate and save model after update or insertion.
* Use the reference node and tag to rename validation output for a specific
* node to a new offset, which makes it easier to reference specific uuids
* without having to use them in the frontend descriptions.
* @param $mdl model reference
* @param $node reference node, to use as relative offset
* @param $reference reference for validation output, used to rename the validation output keys
* @return array result / validation output
*/
private function save($mdl, $node = null, $reference = null)
{
$result = array("result"=>"failed","validations" => array());
// perform validation
$valMsgs = $mdl->performValidation();
foreach ($valMsgs as $field => $msg) {
// replace absolute path to attribute for relative one at uuid.
if ($node != null) {
$fieldnm = str_replace($node->__reference, $reference, $msg->getField());
$result["validations"][$fieldnm] = $msg->getMessage();
} else {
$result["validations"][$msg->getField()] = $msg->getMessage();
}
}
// serialize model to config and save when there are no validation errors
if (count($result['validations']) == 0) {
// save config if validated correctly
$mdl->serializeToConfig();
Config::getInstance()->save();
$result = array("result" => "saved");
}
return $result;
}
/**
* retrieve certificate settings or return defaults
* @param $uuid item unique id
* @return array
*/
public function getAction($uuid = null)
{
$mdlAcme = new AcmeClient();
if ($uuid != null) {
$node = $mdlAcme->getNodeByReference('certificates.certificate.'.$uuid);
if ($node != null) {
// return node
return array("certificate" => $node->getNodes());
}
} else {
// generate new node, but don't save to disc
$node = $mdlAcme->certificates->certificate->add() ;
return array("certificate" => $node->getNodes());
}
return array();
}
/**
* update certificate with given properties
* @param $uuid item unique id
* @return array
*/
public function setAction($uuid)
{
if ($this->request->isPost() && $this->request->hasPost("certificate")) {
$mdlAcme = new AcmeClient();
if ($uuid != null) {
$node = $mdlAcme->getNodeByReference('certificates.certificate.'.$uuid);
if ($node != null) {
$node->setNodes($this->request->getPost("certificate"));
return $this->save($mdlAcme, $node, "certificate");
}
}
}
return array("result"=>"failed");
}
/**
* add new certificate and set with attributes from post
* @return array
*/
public function addAction()
{
$result = array("result"=>"failed");
if ($this->request->isPost() && $this->request->hasPost("certificate")) {
$mdlAcme = new AcmeClient();
$node = $mdlAcme->certificates->certificate->Add();
$node->setNodes($this->request->getPost("certificate"));
return $this->save($mdlAcme, $node, "certificate");
}
return $result;
}
/**
* delete certificate by uuid
* @param $uuid item unique id
* @return array status
*/
public function delAction($uuid)
{
$result = array("result"=>"failed");
if ($this->request->isPost()) {
$mdlAcme = new AcmeClient();
if ($uuid != null) {
if ($mdlAcme->certificates->certificate->del($uuid)) {
// if item is removed, serialize to config and save
$mdlAcme->serializeToConfig();
Config::getInstance()->save();
$result['result'] = 'deleted';
} else {
$result['result'] = 'not found';
}
}
}
return $result;
}
/**
* toggle certificate 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 toggleAction($uuid, $enabled = null)
{
$result = array("result" => "failed");
if ($this->request->isPost()) {
$mdlAcme = new AcmeClient();
if ($uuid != null) {
$node = $mdlAcme->getNodeByReference('certificates.certificate.' . $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
$mdlAcme->serializeToConfig();
Config::getInstance()->save();
}
}
}
return $result;
}
/**
* search certificates
* @return array
*/
public function searchAction()
{
$this->sessionClose();
$mdlAcme = new AcmeClient();
$grid = new UIModelGrid($mdlAcme->certificates->certificate);
return $grid->fetchBindRequest(
$this->request,
array("enabled", "name", "altNames", "description","certificateid"),
"name"
);
}
/**
* sign certificate by uuid
* @param $uuid item unique id
* @return array status
*/
public function signAction($uuid)
{
$result = array("result"=>"failed");
if ($this->request->isPost()) {
$mdlAcme = new AcmeClient();
if ($uuid != null) {
$node = $mdlAcme->getNodeByReference('certificates.certificate.' . $uuid);
if ($node != null) {
$cert_id = $node->id;
$backend = new Backend();
$response = $backend->configdRun("acmeclient sign-cert {$cert_id}");
return array("response" => $response);
}
}
}
return $result;
}
/**
* revoke certificate by uuid
* @param $uuid item unique id
* @return array status
*/
public function revokeAction($uuid)
{
$result = array("result"=>"failed");
if ($this->request->isPost()) {
$mdlAcme = new AcmeClient();
if ($uuid != null) {
$node = $mdlAcme->getNodeByReference('certificates.certificate.' . $uuid);
if ($node != null) {
$cert_id = $node->id;
$backend = new Backend();
$response = $backend->configdRun("acmeclient revoke-cert {$cert_id}");
return array("response" => $response);
}
}
}
return $result;
}
}
@@ -0,0 +1,191 @@
<?php
/**
* Copyright (C) 2017 Frank Wall
* Copyright (C) 2015 Deciso B.V.
*
* 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\AcmeClient\Api;
use \OPNsense\Base\ApiControllerBase;
use \OPNsense\Core\Backend;
use \OPNsense\Core\Config;
use \OPNsense\Cron\Cron;
use \OPNsense\AcmeClient\AcmeClient;
/**
* Class ServiceController
* @package OPNsense\AcmeClient
*/
class ServiceController extends ApiControllerBase
{
/**
* start acmeclient service (in background)
* @return array
*/
public function startAction()
{
if ($this->request->isPost()) {
$backend = new Backend();
$response = $backend->configdRun("acmeclient http-start", true);
return array("response" => $response);
} else {
return array("response" => array());
}
}
/**
* stop acmeclient service
* @return array
*/
public function stopAction()
{
if ($this->request->isPost()) {
$backend = new Backend();
$response = $backend->configdRun("acmeclient http-stop");
return array("response" => $response);
} else {
return array("response" => array());
}
}
/**
* restart acme_http_challenge service
* @return array
*/
public function restartAction()
{
if ($this->request->isPost()) {
$backend = new Backend();
$response = $backend->configdRun("acmeclient http-restart");
return array("response" => $response);
} else {
return array("response" => array());
}
}
/**
* retrieve status of acme_http_challenge service
* @return array
* @throws \Exception
*/
public function statusAction()
{
$backend = new Backend();
$model = new AcmeClient();
$response = $backend->configdRun("acmeclient http-status");
if (strpos($response, "not running") > 0) {
if ($model->settings->enabled->__toString() == 1) {
$status = "stopped";
} else {
$status = "disabled";
}
} elseif (strpos($response, "is running") > 0) {
$status = "running";
} elseif ($model->settings->enabled->__toString() == 0) {
$status = "disabled";
} else {
$status = "unkown";
}
return array("status" => $status);
}
/**
* reconfigure acmeclient, generate config and reload
*/
public function reconfigureAction()
{
if ($this->request->isPost()) {
// close session for long running action
$this->sessionClose();
$force_restart = false;
$mdlAcme = new AcmeClient();
$backend = new Backend();
$runStatus = $this->statusAction();
// stop acmeclient when disabled
if ($runStatus['status'] == "running" &&
($mdlAcme->settings->enabled->__toString() == 0 || $force_restart)) {
$this->stopAction();
}
// generate template
$backend->configdRun('template reload OPNsense/AcmeClient');
// now setup the environment
$backend->configdRun("acmeclient setup");
// (res)start daemon
if ($mdlAcme->settings->enabled->__toString() == 1) {
if ($runStatus['status'] == "running" && !$force_restart) {
$backend->configdRun("acmeclient http-restart");
} else {
$this->startAction();
}
}
return array("status" => "ok");
} else {
return array("status" => "failed");
}
}
/**
* run syntax check for our custom lighttpd configuration
* @return array
* @throws \Exception
*/
public function configtestAction()
{
$backend = new Backend();
// first generate template based on current configuration
$backend->configdRun('template reload OPNsense/AcmeClient');
// now setup the environment
$backend->configdRun("acmeclient setup");
// finally run the syntax check
$response = $backend->configdRun("acmeclient configtest");
return array("result" => $response);
// TODO: We may also want to check for duplicate cert names, etc.
}
/**
* Run sign or renew (if required) command for ALL certificates
* @return array
* @throws \Exception
*/
public function signallcertsAction()
{
$backend = new Backend();
// first setup the environment
$backend->configdRun("acmeclient setup");
// run the command
$response = $backend->configdRun("acmeclient sign-all-certs");
return array("result" => $response);
}
}
@@ -0,0 +1,116 @@
<?php
/**
* Copyright (C) 2017 Frank Wall
* Copyright (C) 2015 Deciso B.V.
*
* 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\AcmeClient\Api;
use \OPNsense\Base\ApiMutableModelControllerBase;
use \OPNsense\Core\Backend;
use \OPNsense\Cron\Cron;
use \OPNsense\Core\Config;
use \OPNsense\Base\UIModelGrid;
/**
* Class SettingsController
* @package OPNsense\AcmeClient
*/
class SettingsController extends ApiMutableModelControllerBase
{
static protected $internalModelName = 'acmeclient';
static protected $internalModelClass = '\OPNsense\AcmeClient\AcmeClient';
/**
* create new cron job or return already available one
* @return array status action
*/
public function fetchRBCronAction()
{
$result = array("result" => "no change");
// TODO: How to force the system to write-out the cronjob?
if ($this->request->isPost()) {
$mdlAcme = $this->getModel();
$backend = new Backend();
// Setup cronjob if AcmeClient and AutoRenewal is enabled.
if ((string)$mdlAcme->settings->UpdateCron == "" and
(string)$mdlAcme->settings->autoRenewal == "1" and
(string)$mdlAcme->settings->enabled == "1") {
$mdlCron = new Cron();
// NOTE: Only configd actions are valid commands for cronjobs
// and they *must* provide a description that is not empty.
$cron_uuid = $mdlCron->newDailyJob(
"AcmeClient",
"acmeclient cron-auto-renew",
"AcmeClient Cronjob for Certificate AutoRenewal",
"*",
"1"
);
$mdlAcme->settings->UpdateCron = $cron_uuid;
// Save updated configuration.
if ($mdlCron->performValidation()->count() == 0) {
$mdlCron->serializeToConfig();
// save data to config, do not validate because the current in memory model doesn't know about the
// cron item just created.
$mdlAcme->serializeToConfig($validateFullModel = false, $disable_validation = true);
Config::getInstance()->save();
// Regenerate the crontab
$backend->configdRun('template reload OPNsense/Cron');
$result['result'] = "new";
$result['uuid'] = $cron_uuid;
} else {
$result['result'] = "unable to add cron";
}
// Delete cronjob if AcmeClient or AutoRenewal is disabled.
} elseif ((string)$mdlAcme->settings->UpdateCron != "" and
((string)$mdlAcme->settings->autoRenewal == "0" or
(string)$mdlAcme->settings->enabled == "0")) {
$cron_uuid = (string)$mdlAcme->settings->UpdateCron;
$mdlAcme->settings->UpdateCron = null;
$mdlCron = new Cron();
if ($mdlCron->jobs->job->del($cron_uuid)) {
// if item is removed, serialize to config and save
$mdlCron->serializeToConfig();
$mdlAcme->serializeToConfig($validateFullModel = false, $disable_validation = true);
Config::getInstance()->save();
// Regenerate the crontab
$backend->configdRun('template reload OPNsense/Cron');
$result['result'] = "deleted";
} else {
$result['result'] = "unable to delete cron";
}
}
}
return $result;
}
}
@@ -0,0 +1,209 @@
<?php
/**
* Copyright (C) 2017 Frank Wall
* Copyright (C) 2015 Deciso B.V.
*
* 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\AcmeClient\Api;
use \OPNsense\Base\ApiControllerBase;
use \OPNsense\AcmeClient\AcmeClient;
use \OPNsense\Core\Config;
use \OPNsense\Base\UIModelGrid;
/**
* Class ValidationsController
* @package OPNsense\AcmeClient
*/
class ValidationsController extends ApiControllerBase
{
/**
* Validate and save model after update or insertion.
* Use the reference node and tag to rename validation output for a specific
* node to a new offset, which makes it easier to reference specific uuids
* without having to use them in the frontend descriptions.
* @param $mdl model reference
* @param $node reference node, to use as relative offset
* @param $reference reference for validation output, used to rename the validation output keys
* @return array result / validation output
*/
private function save($mdl, $node = null, $reference = null)
{
$result = array("result"=>"failed","validations" => array());
// perform validation
$valMsgs = $mdl->performValidation();
foreach ($valMsgs as $field => $msg) {
// replace absolute path to attribute for relative one at uuid.
if ($node != null) {
$fieldnm = str_replace($node->__reference, $reference, $msg->getField());
$result["validations"][$fieldnm] = $msg->getMessage();
} else {
$result["validations"][$msg->getField()] = $msg->getMessage();
}
}
// serialize model to config and save when there are no validation errors
if (count($result['validations']) == 0) {
// save config if validated correctly
$mdl->serializeToConfig();
Config::getInstance()->save();
$result = array("result" => "saved");
}
return $result;
}
/**
* retrieve validation settings or return defaults
* @param $uuid item unique id
* @return array
*/
public function getAction($uuid = null)
{
$mdlAcme = new AcmeClient();
if ($uuid != null) {
$node = $mdlAcme->getNodeByReference('validations.validation.'.$uuid);
if ($node != null) {
// return node
return array("validation" => $node->getNodes());
}
} else {
// generate new node, but don't save to disc
$node = $mdlAcme->validations->validation->add() ;
return array("validation" => $node->getNodes());
}
return array();
}
/**
* update validation with given properties
* @param $uuid item unique id
* @return array
*/
public function setAction($uuid)
{
if ($this->request->isPost() && $this->request->hasPost("validation")) {
$mdlAcme = new AcmeClient();
if ($uuid != null) {
$node = $mdlAcme->getNodeByReference('validations.validation.'.$uuid);
if ($node != null) {
$node->setNodes($this->request->getPost("validation"));
return $this->save($mdlAcme, $node, "validation");
}
}
}
return array("result"=>"failed");
}
/**
* add new validation and set with attributes from post
* @return array
*/
public function addAction()
{
$result = array("result"=>"failed");
if ($this->request->isPost() && $this->request->hasPost("validation")) {
$mdlAcme = new AcmeClient();
$node = $mdlAcme->validations->validation->Add();
$node->setNodes($this->request->getPost("validation"));
return $this->save($mdlAcme, $node, "validation");
}
return $result;
}
/**
* delete validation by uuid
* @param $uuid item unique id
* @return array status
*/
public function delAction($uuid)
{
$result = array("result"=>"failed");
if ($this->request->isPost()) {
$mdlAcme = new AcmeClient();
if ($uuid != null) {
if ($mdlAcme->validations->validation->del($uuid)) {
// if item is removed, serialize to config and save
$mdlAcme->serializeToConfig();
Config::getInstance()->save();
$result['result'] = 'deleted';
} else {
$result['result'] = 'not found';
}
}
}
return $result;
}
/**
* toggle validation 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 toggleAction($uuid, $enabled = null)
{
$result = array("result" => "failed");
if ($this->request->isPost()) {
$mdlAcme = new AcmeClient();
if ($uuid != null) {
$node = $mdlAcme->getNodeByReference('validations.validation.' . $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
$mdlAcme->serializeToConfig();
Config::getInstance()->save();
}
}
}
return $result;
}
/**
* search validations
* @return array
*/
public function searchAction()
{
$this->sessionClose();
$mdlAcme = new AcmeClient();
$grid = new UIModelGrid($mdlAcme->validations->validation);
return $grid->fetchBindRequest(
$this->request,
array("enabled", "name", "description","validationid"),
"name"
);
}
}
@@ -0,0 +1,46 @@
<?php
/**
* Copyright (C) 2017 Frank Wall
* Copyright (C) 2015 Deciso B.V.
*
* 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\AcmeClient;
/**
* Class CertificatesController
* @package OPNsense\AcmeClient
*/
class CertificatesController extends \OPNsense\Base\IndexController
{
public function indexAction()
{
$this->view->title = "Let's Encrypt Certificates";
// include form definitions
$this->view->formDialogCertificate = $this->getForm("dialogCertificate");
// choose template
$this->view->pick('OPNsense/AcmeClient/certificates');
}
}
@@ -0,0 +1,51 @@
<?php
/**
* Copyright (C) 2017 Frank Wall
* Copyright (C) 2015 Deciso B.V.
*
* 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\AcmeClient;
/**
* Class IndexController
* @package OPNsense\AcmeClient
*/
class IndexController extends \OPNsense\Base\IndexController
{
/**
* acme-client index page
* @throws \Exception
*/
public function indexAction()
{
// set page title
$this->view->title = "Let's Encrypt Settings";
// include form definitions
$this->view->settingsForm = $this->getForm("settings");
// pick the template to serve
$this->view->pick('OPNsense/AcmeClient/settings');
}
}
@@ -0,0 +1,46 @@
<?php
/**
* Copyright (C) 2017 Frank Wall
* Copyright (C) 2015 Deciso B.V.
*
* 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\AcmeClient;
/**
* Class ValidationsController
* @package OPNsense\AcmeClient
*/
class ValidationsController extends \OPNsense\Base\IndexController
{
public function indexAction()
{
$this->view->title = "Let's Encrypt Domain Validation Methods";
// include form definitions
$this->view->formDialogValidation = $this->getForm("dialogValidation");
// choose template
$this->view->pick('OPNsense/AcmeClient/validations');
}
}
@@ -0,0 +1,33 @@
<form>
<field>
<id>account.enabled</id>
<label>Enabled</label>
<type>checkbox</type>
<help>Enable this account</help>
</field>
<field>
<id>account.name</id>
<label>Name</label>
<type>text</type>
<help>Name to identify this account.</help>
</field>
<field>
<id>account.description</id>
<label>Description</label>
<type>text</type>
<help>Description for this account.</help>
</field>
<field>
<id>account.email</id>
<label>E-Mail Address</label>
<type>text</type>
<help>Optional e-mail address for this account.</help>
</field>
<field>
<id>account.certificateAuthority</id>
<label>Certificate Authority</label>
<type>dropdown</type>
<help><![CDATA[Select the certificate authority for this account.]]></help>
<advanced>true</advanced>
</field>
</form>
@@ -0,0 +1,53 @@
<form>
<field>
<id>certificate.enabled</id>
<label>Enabled</label>
<type>checkbox</type>
<help>Enable this certificate</help>
</field>
<field>
<id>certificate.name</id>
<label>Name</label>
<type>text</type>
<help>Name to identify this certificate.</help>
</field>
<field>
<id>certificate.description</id>
<label>Description</label>
<type>text</type>
<help>Description for this certificate.</help>
</field>
<field>
<id>certificate.altNames</id>
<label>Alt Names</label>
<type>select_multiple</type>
<style>tokenize</style>
<allownew>true</allownew>
<help><![CDATA[Configure additional names that should be part pf the certificate, i.e. www.example.com or mail.example.com. Use TAB key to complete typing a FQDN.<br/><div class="text-info"><b>NOTE:</b>Cannot be altered once the certificate was signed by the Let's Encrypt Authority! You need to create a new certificate to add additional names.</div>]]></help>
<hint>Enter FQDN here. Finish with TAB.</hint>
</field>
<field>
<id>certificate.account</id>
<label>CA Account</label>
<type>dropdown</type>
<help><![CDATA[Set the CA account to use for this certificate.]]></help>
</field>
<field>
<id>certificate.validationMethod</id>
<label>Validation Method</label>
<type>dropdown</type>
<help><![CDATA[Set the Let's Encrypt validation method for this certificate.]]></help>
</field>
<field>
<id>certificate.autoRenewal</id>
<label>Auto Renewal</label>
<type>checkbox</type>
<help>Enable automatic renewal for this certificate to prevent expiration.</help>
</field>
<field>
<id>certificate.renewInterval</id>
<label>Renewal Interval</label>
<type>text</type>
<help><![CDATA[Specifies the days to renew the cert. The max value is 60 days.]]></help>
</field>
</form>
@@ -0,0 +1,357 @@
<form>
<field>
<id>validation.enabled</id>
<label>Enabled</label>
<type>checkbox</type>
<help>Enable this validation</help>
</field>
<field>
<id>validation.name</id>
<label>Name</label>
<type>text</type>
<help>Name to identify this validation.</help>
</field>
<field>
<id>validation.description</id>
<label>Description</label>
<type>text</type>
<help>Description for this validation.</help>
</field>
<field>
<id>validation.method</id>
<label>Validation Method</label>
<type>dropdown</type>
<help><![CDATA[Set the Let's Encrypt validation method.]]></help>
</field>
<field>
<label>HTTP-01</label>
<type>header</type>
</field>
<field>
<id>validation.http_service</id>
<label>HTTP Service</label>
<type>dropdown</type>
<help></help>
</field>
<field>
<label>HTTP-01/OPNsense</label>
<type>header</type>
</field>
<field>
<id>validation.http_opn_autodiscovery</id>
<label>IP Auto-Discovery</label>
<type>checkbox</type>
<help><![CDATA[The FQDN's used in your certificate must currently point to an official IP address. Choose this option to let OPNsense tryo to auto-discover these IP addresses. This will lead to a short downtime of the service that is normally used with this IP address.<br/><div class="text-info"><b>NOTE:</b>This will ONLY work if the official IP addresses are LOCALLY configured on your OPNsense firewall.</div>]]></help>
</field>
<field>
<id>validation.http_opn_interface</id>
<label>Interface</label>
<type>dropdown</type>
<help><![CDATA[The FQDN's used in your certificate must currently point to an official IP address. Choose the interface where this IP address is currently configured. OPNsense will automatically create a temporary port forward to allow the Let's Encrypt validation to succeed. This will lead to a short downtime of the service that is normally used with this IP address.<br/><div class="text-info"><b>NOTE:</b>This will ONLY work if the official IP addresses are LOCALLY configured on your OPNsense firewall.</div>]]></help>
</field>
<field>
<id>validation.http_opn_ipaddresses</id>
<label>IP Addresses</label>
<type>select_multiple</type>
<style>tokenize</style>
<allownew>true</allownew>
<help><![CDATA[The FQDN's used in your certificate must currently point to one or more official IP addresses. Enter the all of these IP addresses here. OPNsense will automatically create a temporary port forward to allow the Let's Encrypt validation to succeed. This will lead to a short downtime of the service that is normally used with these IP addresses.<br/><div class="text-info"><b>NOTE:</b>This will ONLY work if the official IP addresses are LOCALLY configured on your OPNsense firewall.</div>]]></help>
<hint>Enter IP addresses here. Finish each with TAB.</hint>
</field>
<!--
<field>
<id>validation.http_haproxyInject</id>
<label>HAProxy Config Injection</label>
<type>checkbox</type>
<help>Automatically inject config into the local HAProxy instance to let it serve acme challanges without service interruption.</help>
</field>
<field>
<id>validation.http_haproxyFrontend</id>
<label>HAProxy Frontend</label>
<type>dropdown</type>
<help>Choose the local HAProxy frontend that should be configured to server acme challenges.</help>
</field>
<field>
<id>validation.http_relaydInject</id>
<label>Loadbalancer Config Injection</label>
<type>checkbox</type>
<help>Automatically inject config into the local Loadbalancer (relayd) to let it serve acme challanges without service interruption.</help>
</field>
<field>
<id>validation.http_relaydVserver</id>
<label>Loadbalancer Virtual Server</label>
<type>text</type>
<help>Choose the Virtual Server from the relayd Loadbalancer that should be configured to server acme challenges.</help>
</field>
-->
<field>
<label>DNS-01</label>
<type>header</type>
</field>
<field>
<id>validation.dns_service</id>
<label>DNS Service</label>
<type>dropdown</type>
<help></help>
</field>
<field>
<id>validation.dns_sleep</id>
<label>Sleep Time</label>
<type>text</type>
<help><![CDATA[The time in seconds to wait for all the TXT records to take effect DNS API mode. Default 120 seconds.]]></help>
</field>
<field>
<label>DNS-01/ad</label>
<type>header</type>
</field>
<field>
<id>validation.dns_ad_key</id>
<label>Key</label>
<type>text</type>
<help></help>
</field>
<field>
<label>DNS-01/ali</label>
<type>header</type>
</field>
<field>
<id>validation.dns_ali_key</id>
<label>Key</label>
<type>text</type>
<help></help>
</field>
<field>
<id>validation.dns_ali_secret</id>
<label>Secret</label>
<type>text</type>
<help></help>
</field>
<field>
<label>DNS-01/AWS Route53</label>
<type>header</type>
</field>
<field>
<id>validation.dns_aws_id</id>
<label>AWS ID</label>
<type>text</type>
<help></help>
</field>
<field>
<id>validation.dns_aws_secret</id>
<label>AWS Secret</label>
<type>text</type>
<help></help>
</field>
<field>
<label>DNS-01/Cloudflare</label>
<type>header</type>
</field>
<field>
<id>validation.dns_cf_email</id>
<label>CF E-Mail</label>
<type>text</type>
<help></help>
</field>
<field>
<id>validation.dns_cf_key</id>
<label>CF Key</label>
<type>text</type>
<help></help>
</field>
<field>
<label>DNS-01/CX</label>
<type>header</type>
</field>
<field>
<id>validation.dns_cx_key</id>
<label>Key</label>
<type>text</type>
<help></help>
</field>
<field>
<id>validation.dns_cx_secret</id>
<label>Secret</label>
<type>text</type>
<help></help>
</field>
<field>
<label>DNS-01/DP</label>
<type>header</type>
</field>
<field>
<id>validation.dns_dp_id</id>
<label>ID</label>
<type>text</type>
<help></help>
</field>
<field>
<id>validation.dns_dp_key</id>
<label>Key</label>
<type>text</type>
<help></help>
</field>
<field>
<label>DNS-01/GD</label>
<type>header</type>
</field>
<field>
<id>validation.dns_gd_key</id>
<label>Key</label>
<type>text</type>
<help></help>
</field>
<field>
<id>validation.dns_gd_secret</id>
<label>Secret</label>
<type>text</type>
<help></help>
</field>
<field>
<label>DNS-01/IPSConfig</label>
<type>header</type>
</field>
<field>
<id>validation.dns_ispconfig_user</id>
<label>User</label>
<type>text</type>
<help></help>
</field>
<field>
<id>validation.dns_ispconfig_password</id>
<label>Password</label>
<type>text</type>
<help></help>
</field>
<field>
<id>validation.dns_ispconfig_api</id>
<label>API URL</label>
<type>text</type>
<help></help>
</field>
<field>
<id>validation.dns_ispconfig_insecure</id>
<label>Disable SSL Verification</label>
<type>text</type>
<help></help>
</field>
<field>
<label>DNS-01/lexicon</label>
<type>header</type>
</field>
<field>
<id>validation.dns_lexicon_provider</id>
<label>Provider</label>
<type>dropdown</type>
<help></help>
</field>
<field>
<id>validation.dns_lexicon_user</id>
<label>User</label>
<type>text</type>
<help></help>
</field>
<field>
<id>validation.dns_lexicon_token</id>
<label>Token</label>
<type>text</type>
<help></help>
</field>
<field>
<label>DNS-01/lua</label>
<type>header</type>
</field>
<field>
<id>validation.dns_lua_email</id>
<label>E-Mail</label>
<type>text</type>
<help></help>
</field>
<field>
<id>validation.dns_lua_key</id>
<label>Key</label>
<type>text</type>
<help></help>
</field>
<field>
<label>DNS-01/ME</label>
<type>header</type>
</field>
<field>
<id>validation.dns_me_key</id>
<label>Key</label>
<type>text</type>
<help></help>
</field>
<field>
<id>validation.dns_me_secret</id>
<label>Secret</label>
<type>text</type>
<help></help>
</field>
<field>
<label>DNS-01/nsupdate</label>
<type>header</type>
</field>
<field>
<id>validation.dns_nsupdate_server</id>
<label>Server (FQDN)</label>
<type>text</type>
<help></help>
</field>
<field>
<id>validation.dns_nsupdate_key</id>
<label>Secret Key</label>
<type>textbox</type>
<help></help>
</field>
<field>
<label>DNS-01/OVH</label>
<type>header</type>
</field>
<field>
<id>validation.dns_ovh_app_key</id>
<label>Application Key</label>
<type>text</type>
<help></help>
</field>
<field>
<id>validation.dns_ovh_app_secret</id>
<label>Application Secret</label>
<type>text</type>
<help></help>
</field>
<field>
<id>validation.dns_ovh_consumer_key</id>
<label>Consumer Key</label>
<type>text</type>
<help></help>
</field>
<field>
<id>validation.dns_ovh_endpoint</id>
<label>Endpoint</label>
<type>text</type>
<help><![CDATA[Specify the OVH endpoint, i.e. ovh-eu, ovh-ca, kimsufi-eu, etc. Please refer to the <a href="https://github.com/Neilpang/acme.sh/tree/master/dnsapi">acme.sh documentation</a> for further information.]]></help>
</field>
<field>
<label>DNS-01/PowerDNS</label>
<type>header</type>
</field>
<field>
<id>validation.dns_pdns_url</id>
<label>URL</label>
<type>text</type>
<help><![CDATA[Specify the URL for your PowerDNS server, i.e. http://ns.example.com:8081.]]></help>
</field>
<field>
<id>validation.dns_pdns_serverid</id>
<label>Server ID</label>
<type>text</type>
<help><![CDATA[Specify the Server ID of your PowerDNS server, i.e. localhost.]]></help>
</field>
<field>
<id>validation.dns_pdns_token</id>
<label>Token</label>
<type>text</type>
<help></help>
</field>
</form>
@@ -0,0 +1,27 @@
<form>
<field>
<id>acmeclient.settings.enabled</id>
<label>Enable Plugin</label>
<type>checkbox</type>
<help><![CDATA[Enable Let's Encrypt plugin]]></help>
</field>
<field>
<id>acmeclient.settings.autoRenewal</id>
<label>Auto Renewal</label>
<type>checkbox</type>
<help><![CDATA[Enable automatic renewal for certificates to prevent expiration.]]></help>
</field>
<field>
<id>acmeclient.settings.environment</id>
<label>Let's Encrypt Environment</label>
<type>dropdown</type>
<help><![CDATA[Choose Let's Encrypts staging environment when using it for the first time or while testing new validation methods. The staging environment offers <a href="https://letsencrypt.org/docs/staging-environment/">relaxed rate limits</a>.<br/><div class="text-info"><b>NOTE:</b>Certificates signed by the staging environment are NOT valid. You need to forcefully re-sign (or delete and re-create) them after switching from staging to production environment.</div>]]></help>
</field>
<field>
<id>acmeclient.settings.challengePort</id>
<label>Local HTTP Port</label>
<type>text</type>
<help><![CDATA[When using HTTP-01 as validation method, a local webserver is used to provide acme challenge data to the Let's Encrypt servers. This setting allows you to change the local port of this webserver in case it interferes with another local services. Defaults to port 43580.]]></help>
<advanced>true</advanced>
</field>
</form>
@@ -0,0 +1,10 @@
<acl>
<page-services-letsencrypt>
<name>Services: Let's Encrypt</name>
<patterns>
<pattern>ui/acmeclient/*</pattern>
<pattern>api/acmeclient/*</pattern>
<pattern>diag_logs_acmeclient.php</pattern>
</patterns>
</page-services-letsencrypt>
</acl>
@@ -0,0 +1,68 @@
<?php
/**
* Copyright (C) 2017 Frank Wall
* Copyright (C) 2015 Deciso B.V.
*
* 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\AcmeClient;
use OPNsense\Base\BaseModel;
/**
* Class AcmeClient
* @package OPNsense\AcmeClient
*/
class AcmeClient extends BaseModel
{
/**
* retrieve certificate by number
* @param $certificateid certificate number
* @return null|BaseField certificate details
*/
public function getByCertificateID($certificateid)
{
foreach ($this->certificates->certificate->__items as $certificate) {
if ((string)$certificateid === (string)$certificate->certificateid) {
return $certificate;
}
}
return null;
}
/**
* check if module is enabled
* @return bool is the AcmeClient enabled (1 or more active certificates)
*/
public function isEnabled()
{
foreach ($this->certificates->certificate->__items as $certificate) {
if ((string)$certificate->enabled == "1") {
return true;
}
}
return false;
}
}
@@ -0,0 +1,373 @@
<model>
<mount>//OPNsense/AcmeClient</mount>
<version>1.0.0</version>
<description>
a secure Let's Encrypt plugin
</description>
<items>
<settings>
<enabled type="BooleanField">
<default>0</default>
<Required>Y</Required>
</enabled>
<autoRenewal type="BooleanField">
<default>1</default>
<Required>Y</Required>
</autoRenewal>
<UpdateCron type="ModelRelationField">
<Model>
<queues>
<source>OPNsense.Cron.Cron</source>
<items>jobs.job</items>
<display>description</display>
<filters>
<origin>/AcmeClient/</origin>
</filters>
</queues>
</Model>
<ValidationMessage>Related cron not found.</ValidationMessage>
<Required>N</Required>
</UpdateCron>
<environment type="OptionField">
<Required>Y</Required>
<default>prod</default>
<OptionValues>
<prod>Production Environment [default]</prod>
<stg>Staging Environment</stg>
</OptionValues>
</environment>
<challengePort type="IntegerField">
<default>43580</default>
<MinimumValue>1024</MinimumValue>
<MaximumValue>65535</MaximumValue>
<Required>Y</Required>
</challengePort>
</settings>
<accounts>
<account type="ArrayField">
<id type="UniqueIdField">
<Required>N</Required>
</id>
<enabled type="BooleanField">
<default>1</default>
<Required>Y</Required>
</enabled>
<name type="TextField">
<Required>Y</Required>
<mask>/^([0-9a-zA-Z._]){1,255}$/u</mask>
<ValidationMessage>Should be a string between 1 and 255 characters.</ValidationMessage>
</name>
<description type="TextField">
<Required>N</Required>
<mask>/^([\t\n\v\f\r 0-9a-zA-Z.:\-,_()\x{00A0}-\x{FFFF}]){1,255}$/u</mask>
<ValidationMessage>Should be a string between 1 and 255 characters.</ValidationMessage>
</description>
<email type="EmailField">
<Required>N</Required>
</email>
<certificateAuthority type="OptionField">
<Required>Y</Required>
<default>letsencrypt</default>
<OptionValues>
<letsencrypt>Let's Encrypt CA</letsencrypt>
</OptionValues>
</certificateAuthority>
<!-- hidden field; the private key for this account -->
<key type="TextField">
<Required>N</Required>
</key>
<!-- hidden field; last update of this account (unixtime) -->
<lastUpdate type="IntegerField">
<Required>N</Required>
</lastUpdate>
</account>
</accounts>
<certificates>
<certificate type="ArrayField">
<id type="UniqueIdField">
<Required>N</Required>
</id>
<enabled type="BooleanField">
<default>1</default>
<Required>Y</Required>
</enabled>
<name type="TextField">
<Required>Y</Required>
<mask>/^([0-9a-zA-Z._]){1,255}$/u</mask>
<ValidationMessage>Should be a string between 1 and 255 characters.</ValidationMessage>
</name>
<description type="TextField">
<Required>N</Required>
<mask>/^([\t\n\v\f\r 0-9a-zA-Z.:\-,_()\x{00A0}-\x{FFFF}]){1,255}$/u</mask>
<ValidationMessage>Should be a string between 1 and 255 characters.</ValidationMessage>
</description>
<altNames type="CSVListField">
<Required>N</Required>
<multiple>Y</multiple>
<!--- XXX: FQDN should at least contain one dot -->
<mask>/^((([0-9a-zA-Z._\-\*]+\.[0-9a-zA-Z._\-\*]+(-[0-9]+)?)([,]){0,1}))*/u</mask>
<ChangeCase>lower</ChangeCase>
<ValidationMessage>Please provide a valid FQDN, i.e. www.example.com or mail.example.com.</ValidationMessage>
</altNames>
<account type="ModelRelationField">
<Model>
<template>
<source>OPNsense.AcmeClient.AcmeClient</source>
<items>accounts.account</items>
<display>name</display>
</template>
</Model>
<ValidationMessage>Related item not found</ValidationMessage>
<multiple>N</multiple>
<Required>Y</Required>
</account>
<validationMethod type="ModelRelationField">
<Model>
<template>
<source>OPNsense.AcmeClient.AcmeClient</source>
<items>validations.validation</items>
<display>name</display>
</template>
</Model>
<ValidationMessage>Related item not found</ValidationMessage>
<multiple>N</multiple>
<Required>Y</Required>
</validationMethod>
<autoRenewal type="BooleanField">
<default>1</default>
<Required>Y</Required>
</autoRenewal>
<renewInterval type="IntegerField">
<Required>Y</Required>
<MinimumValue>1</MinimumValue>
<MaximumValue>60</MaximumValue>
<default>60</default>
</renewInterval>
<!-- hidden field; ID of the certificate in Cert Manager -->
<certRefId type="TextField">
<Required>N</Required>
</certRefId>
<!-- hidden field; last update of this certificate (unixtime) -->
<lastUpdate type="IntegerField">
<Required>N</Required>
</lastUpdate>
</certificate>
</certificates>
<validations>
<validation type="ArrayField">
<id type="UniqueIdField">
<Required>N</Required>
</id>
<enabled type="BooleanField">
<default>1</default>
<Required>Y</Required>
</enabled>
<name type="TextField">
<Required>Y</Required>
<mask>/^([0-9a-zA-Z._]){1,255}$/u</mask>
<ValidationMessage>Should be a string between 1 and 255 characters.</ValidationMessage>
</name>
<description type="TextField">
<Required>N</Required>
<mask>/^([\t\n\v\f\r 0-9a-zA-Z.:\-,_()\x{00A0}-\x{FFFF}]){1,255}$/u</mask>
<ValidationMessage>Should be a string between 1 and 255 characters.</ValidationMessage>
</description>
<method type="OptionField">
<Required>Y</Required>
<default>http01</default>
<OptionValues>
<http01>HTTP-01</http01>
<dns01>DNS-01</dns01>
</OptionValues>
</method>
<http_service type="OptionField">
<Required>Y</Required>
<default>opnsense</default>
<OptionValues>
<opnsense>OPNsense port forward (specify Interface or IP)</opnsense>
<!-- WIP/TODO
<haproxy>HAProxy Frontend (OPNsense plugin)</haproxy>
<relayd>relayd Loadbalancer Virtual Server (OPNsense plugin)</relayd>
-->
</OptionValues>
</http_service>
<http_opn_autodiscovery type="BooleanField">
<default>1</default>
<Required>N</Required>
</http_opn_autodiscovery>
<!-- XXX: we want something more like get_possible_listen_ips() instead -->
<http_opn_interface type="InterfaceField">
<Required>N</Required>
<default>wan</default>
<filters>
<enable>/^(?!0).*$/</enable>
</filters>
</http_opn_interface>
<http_opn_ipaddresses type="CSVListField">
<Required>N</Required>
<multiple>Y</multiple>
</http_opn_ipaddresses>
<!-- WIP/TODO
<http_haproxyInject type="BooleanField">
<default>1</default>
<Required>N</Required>
</http_haproxyInject>
<http_haproxyFrontend type="ModelRelationField">
<Model>
<template>
<source>OPNsense.HAProxy.HAProxy</source>
<items>frontends.frontend</items>
<display>name</display>
</template>
</Model>
<ValidationMessage>Related item not found</ValidationMessage>
<multiple>N</multiple>
<Required>N</Required>
</http_haproxyFrontend>
<http_relaydInject type="BooleanField">
<default>1</default>
<Required>N</Required>
</http_relaydInject>
<http_relaydVserver type="TextField">
<Required>N</Required>
<ValidationMessage>Should be a string between 1 and 255 characters.</ValidationMessage>
</http_relaydVserver>
-->
<dns_service type="OptionField">
<Required>Y</Required>
<default>dns_nsupdate</default>
<OptionValues>
<dns_ad>Alwaysdata.com API</dns_ad>
<dns_ali>aliyun.com API</dns_ali>
<dns_aws>AWS Route 53</dns_aws>
<dns_cf>CloudFlare.com API</dns_cf>
<dns_cx>CloudXNS.com API</dns_cx>
<dns_dp>DNSPod.cn API</dns_dp>
<dns_gd>GoDaddy.com API</dns_gd>
<dns_ispconfig>ISPConfig 3.1+ API</dns_ispconfig>
<dns_lexicon>lexicon DNS API</dns_lexicon>
<dns_lua>LuaDNS.com API</dns_lua>
<dns_me>DNSMadeEasy.com API</dns_me>
<dns_nsupdate>nsupdate (RFC 2136)</dns_nsupdate>
<dns_ovh>OVH, kimsufi, soyoustart and runabove API</dns_ovh>
<dns_pdns>PowerDNS.com API</dns_pdns>
</OptionValues>
</dns_service>
<dns_sleep type="IntegerField">
<MinimumValue>1</MinimumValue>
<MaximumValue>10000</MaximumValue>
<default>120</default>
<ValidationMessage>Please specify a value between 1 and 10000.</ValidationMessage>
<Required>Y</Required>
</dns_sleep>
<dns_ad_key type="TextField">
<Required>N</Required>
</dns_ad_key>
<dns_ali_key type="TextField">
<Required>N</Required>
</dns_ali_key>
<dns_ali_secret type="TextField">
<Required>N</Required>
</dns_ali_secret>
<dns_aws_id type="TextField">
<Required>N</Required>
</dns_aws_id>
<dns_aws_secret type="TextField">
<Required>N</Required>
</dns_aws_secret>
<dns_cf_email type="TextField">
<Required>N</Required>
</dns_cf_email>
<dns_cf_key type="TextField">
<Required>N</Required>
</dns_cf_key>
<dns_cx_key type="TextField">
<Required>N</Required>
</dns_cx_key>
<dns_cx_secret type="TextField">
<Required>N</Required>
</dns_cx_secret>
<dns_dp_id type="TextField">
<Required>N</Required>
</dns_dp_id>
<dns_dp_key type="TextField">
<Required>N</Required>
</dns_dp_key>
<dns_gd_key type="TextField">
<Required>N</Required>
</dns_gd_key>
<dns_gd_secret type="TextField">
<Required>N</Required>
</dns_gd_secret>
<dns_ispconfig_user type="TextField">
<Required>N</Required>
</dns_ispconfig_user>
<dns_ispconfig_password type="TextField">
<Required>N</Required>
</dns_ispconfig_password>
<dns_ispconfig_api type="TextField">
<Required>N</Required>
</dns_ispconfig_api>
<dns_ispconfig_insecure type="BooleanField">
<Required>N</Required>
<default>1</default>
</dns_ispconfig_insecure>
<dns_lexicon_provider type="OptionField">
<Required>N</Required>
<default>cloudflare</default>
<OptionValues>
<cloudflare>Cloudflare API</cloudflare>
<namesilo>Namesilo API</namesilo>
</OptionValues>
</dns_lexicon_provider>
<dns_lexicon_user type="TextField">
<Required>N</Required>
</dns_lexicon_user>
<dns_lexicon_token type="TextField">
<Required>N</Required>
</dns_lexicon_token>
<dns_lua_email type="TextField">
<Required>N</Required>
</dns_lua_email>
<dns_lua_key type="TextField">
<Required>N</Required>
</dns_lua_key>
<dns_me_key type="TextField">
<Required>N</Required>
</dns_me_key>
<dns_me_secret type="TextField">
<Required>N</Required>
</dns_me_secret>
<dns_nsupdate_server type="TextField">
<Required>N</Required>
</dns_nsupdate_server>
<!-- TODO: maybe we should base64encode this field? -->
<dns_nsupdate_key type="TextField">
<Required>N</Required>
</dns_nsupdate_key>
<dns_ovh_app_key type="TextField">
<Required>N</Required>
</dns_ovh_app_key>
<dns_ovh_app_secret type="TextField">
<Required>N</Required>
</dns_ovh_app_secret>
<dns_ovh_consumer_key type="TextField">
<Required>N</Required>
</dns_ovh_consumer_key>
<dns_ovh_endpoint type="TextField">
<Required>N</Required>
</dns_ovh_endpoint>
<dns_pdns_url type="TextField">
<Required>N</Required>
</dns_pdns_url>
<dns_pdns_serverid type="TextField">
<Required>N</Required>
</dns_pdns_serverid>
<dns_pdns_token type="TextField">
<Required>N</Required>
</dns_pdns_token>
</validation>
</validations>
</items>
</model>
@@ -0,0 +1,17 @@
<menu>
<Services>
<!-- using LE prefix for proper sorting -->
<LEAcmeClient VisibleName="Let's Encrypt" cssClass="fa fa-certificate fa-fw">
<Settings order="10" url="/ui/acmeclient/">
<GeneralSettings VisibleName="Service Settings" url="/ui/acmeclient/#general-settings"/>
</Settings>
<Accounts VisibleName="Accounts" order="20" url="/ui/acmeclient/accounts/">
</Accounts>
<Validations VisibleName="Validation Methods" order="30" url="/ui/acmeclient/validations/">
</Validations>
<Certificates order="40" url="/ui/acmeclient/certificates/">
</Certificates>
<Log VisibleName="Log File" order="50" url="/diag_logs_acmeclient.php"/>
</LEAcmeClient>
</Services>
</menu>
@@ -0,0 +1,87 @@
{#
Copyright (C) 2017 Frank Wall
OPNsense® is Copyright © 2014-2015 by Deciso B.V.
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.
#}
<script type="text/javascript">
$( document ).ready(function() {
/***********************************************************************
* link grid actions
**********************************************************************/
$("#grid-accounts").UIBootgrid(
{ search:'/api/acmeclient/accounts/search',
get:'/api/acmeclient/accounts/get/',
set:'/api/acmeclient/accounts/set/',
add:'/api/acmeclient/accounts/add/',
del:'/api/acmeclient/accounts/del/',
toggle:'/api/acmeclient/accounts/toggle/',
options: {
rowCount:[10,25,50,100,500,1000]
}
}
);
});
</script>
<ul class="nav nav-tabs" data-tabs="tabs" id="maintabs">
<li class="active"><a data-toggle="tab" href="#accounts">{{ lang._('Accounts') }}</a></li>
</ul>
<div class="tab-content content-box tab-content">
<div id="accounts" class="tab-pane fade in active">
<table id="grid-accounts" class="table table-condensed table-hover table-striped table-responsive" data-editDialog="DialogAccount">
<thead>
<tr>
<th data-column-id="enabled" data-width="6em" data-type="string" data-formatter="rowtoggle">{{ lang._('Enabled') }}</th>
<th data-column-id="name" data-type="string">{{ lang._('Name') }}</th>
<th data-column-id="email" data-type="string">{{ lang._('E-Mail') }}</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>
</div>
</div>
{# include dialogs #}
{{ partial("layout_partials/base_dialog",['fields':formDialogAccount,'id':'DialogAccount','label':'Edit Account'])}}

Some files were not shown because too many files have changed in this diff Show More