AcmeClient: Added automation feature: cert upload via sftp

This commit is contained in:
Jürgen Kellerer
2019-08-14 13:15:11 +02:00
parent 3c5ae7dc9f
commit 16e0baff72
7 changed files with 1770 additions and 1 deletions
@@ -31,6 +31,7 @@ namespace OPNsense\AcmeClient\Api;
use \OPNsense\Base\ApiMutableModelControllerBase;
use \OPNsense\Base\UIModelGrid;
use \OPNsense\Core\Backend;
use \OPNsense\Core\Config;
use \OPNsense\AcmeClient\AcmeClient;
@@ -73,4 +74,54 @@ class ActionsController extends ApiMutableModelControllerBase
{
return $this->searchBase('actions.action', array('enabled', 'name', 'description'), 'name');
}
public function sftpGetIdentityAction()
{
$result = ["status" => "unavailable"];
if ($response = $this->callBackend(["show-sftp-identity"], ["sftp_identity_type", "sftp_host"])) {
$result["status"] = "ok";
$result["identity"] = $response;
}
return $result;
}
public function sftpTestConnectionAction()
{
if ($response = $this->callBackend(
["test-sftp-connection"],
["sftp_host", "sftp_host_key", "sftp_port", "sftp_user", "sftp_identity_type", "sftp_remote_path"])) {
return $response;
}
return ["status" => "unavailable"];
}
private function callBackend(array $command, array $arguments = [])
{
if ($this->request->isPost()) {
$backend = new Backend();
foreach ($arguments as $name) {
$command[] = $this->request->getPost($name);
}
$command = array_map(function ($value) {
return escapeshellarg(empty($value = trim($value)) ? "__default_value" : $value);
}, $command);
if ($result = trim($backend->configdRun("acmeclient " . join(" ", $command)))) {
if (preg_match('/^\[.+\]$/ms', $result) || preg_match('/^\{.+\}$/ms', $result)) {
try {
$result = json_decode($result, true, 64, JSON_THROW_ON_ERROR);
} catch (\Exception $ignored) {/*pass as is when json parsing fails*/}
}
return $result;
}
}
return false;
}
}
@@ -40,6 +40,68 @@
<type>text</type>
<help>Access token for Highwinds API.</help>
</field>
<field>
<label>Required Parameters</label>
<type>header</type>
<style>method_table method_table_upload_sftp</style>
</field>
<field>
<id>action.sftp_host</id>
<label>SFTP Host</label>
<type>text</type>
<help>IP address or hostname of the SFTP server.</help>
</field>
<field>
<id>action.sftp_port</id>
<label>SFTP Port</label>
<type>text</type>
<help>SFTP server port. Leave blank to use default "22".</help>
</field>
<field>
<id>action.sftp_host_key</id>
<label>Host Key</label>
<type>text</type>
<help>SFTP server host key, formatted as in 'known_hosts'.
Leave blank to auto accept host key on first connect (not as secure as specifying it).</help>
</field>
<field>
<id>action.sftp_user</id>
<label>Username</label>
<type>text</type>
<help>The username to login to the SFTP server.</help>
</field>
<field>
<id>action.sftp_identity_type</id>
<label>Identity Type</label>
<type>dropdown</type>
<help>The type of identify to present to the SFTP server for authorization. Select 'none' to use default "ECDSA".</help>
</field>
<field>
<id>action.sftp_remote_path</id>
<label>Remote Path</label>
<type>text</type>
<help>Path on the SFTP server to change to after login.
The path can be absolute or relative to home and must exist.
Leave blank to not change path after login.</help>
</field>
<field>
<id>action.sftp_chmod</id>
<label>Permission (Public Keys)</label>
<type>text</type>
<help>Unix permission to apply to uploaded public keys. Leave blank to use default "0440".</help>
</field>
<field>
<id>action.sftp_chmod_key</id>
<label>Permission (Private Keys)</label>
<type>text</type>
<help>Unix permission to apply to uploaded private keys. Leave blank to use default "0400".</help>
</field>
<field>
<id>action.sftp_chgrp</id>
<label>Group</label>
<type>text</type>
<help>Unix group id to apply to all uploaded files. Leave blank to not change the group.</help>
</field>
<field>
<label>Required Parameters</label>
<type>header</type>
@@ -738,6 +738,7 @@
<restart_haproxy>Restart HAProxy (OPNsense plugin)</restart_haproxy>
<restart_nginx>Restart Nginx (OPNsense plugin)</restart_nginx>
<upload_highwinds>Upload certificate to Highwinds CDN</upload_highwinds>
<upload_sftp>Upload certificate via SFTP</upload_sftp>
<configd>System or Plugin Command (select below)</configd>
</OptionValues>
</type>
@@ -751,6 +752,58 @@
<mask>/^.{1,1024}$/u</mask>
<ValidationMessage>Should be a string between 1 and 1024 characters.</ValidationMessage>
</highwinds_access_token>
<sftp_host type="TextField">
<Required>N</Required>
<mask>/^.{1,255}$/u</mask>
<ValidationMessage>Should be a string between 1 and 255 characters.</ValidationMessage>
</sftp_host>
<sftp_host_key type="TextField">
<Required>N</Required>
<!-- Key format: (comment)? key-type :SPACE: key-base64 (:SPACE: comment)?
Reference: https://stackoverflow.com/a/475217 -->
<mask>/^.+?\s(?:[a-z0-9+\/]{4})*(?:[a-z0-9+\/]{2}==|[a-z0-9+\/]{3}=)?(?:\s.+?)?$/i</mask>
<ValidationMessage>Should be a valid public SSH host key (see "known_hosts").</ValidationMessage>
</sftp_host_key>
<sftp_port type="IntegerField">
<Required>N</Required>
<MinimumValue>1</MinimumValue>
<MaximumValue>49151</MaximumValue>
<default>22</default>
<ValidationMessage>Should be a valid port number between 1 and 49151.</ValidationMessage>
</sftp_port>
<sftp_user type="TextField">
<Required>N</Required>
<mask>/^.{1,128}$/u</mask>
<ValidationMessage>Should be a string between 1 and 128 characters.</ValidationMessage>
</sftp_user>
<sftp_identity_type type="OptionField">
<Required>N</Required>
<OptionValues>
<ecdsa>ECDSA</ecdsa>
<rsa>RSA</rsa>
<ed25519>ed25519</ed25519>
</OptionValues>
</sftp_identity_type>
<sftp_remote_path type="TextField">
<Required>N</Required>
<mask>/^.{1,512}$/u</mask>
<ValidationMessage>Should be a string between 1 and 512 characters.</ValidationMessage>
</sftp_remote_path>
<sftp_chgrp type="TextField">
<Required>N</Required>
<mask>/^[0-9]+$/u</mask>
<ValidationMessage>Should be a numeric value.</ValidationMessage>
</sftp_chgrp>
<sftp_chmod type="TextField">
<Required>N</Required>
<mask>/^0[0-9]{3}$/u</mask>
<ValidationMessage>A unix permission, 4 digits (e.g. 0440).</ValidationMessage>
</sftp_chmod>
<sftp_chmod_key type="TextField">
<Required>N</Required>
<mask>/^0[0-9]{3}$/u</mask>
<ValidationMessage>A unix permission, 4 digits (e.g. 0400).</ValidationMessage>
</sftp_chmod_key>
<configd type="ConfigdActionsField">
<filters>
<description>/^(?!.*(Let\'s\ Encrypt|acme|[fF]irmware))([\S\s]{1,255})/</description>
@@ -60,7 +60,135 @@ POSSIBILITY OF SUCH DAMAGE.
$(".method_table_"+$(this).val()).show();
});
$("#action\\.type").change();
})
});
// Helpers for extra buttons and status divs
function makeButton(label, buttonGroup, buttonClass) {
var button = $('<button class="btn" type="button">'
+ '<span class="btn-text"></span>'
+ '<i class="fa fa-spinner fa-pulse" style="margin-left: 0.5em;"></i></button>');
button.addClass(buttonClass || "btn-primary");
$('.fa-spinner', button).hide();
$('.btn-text', button).html(label);
var targetContainer = $("#DialogAction .modal-footer"),
targetId = "method_table_" + buttonGroup,
target = $("." + targetId, targetContainer);
if (!target.is('span')) {
target = $('<span class="method_table" style="float: left"></span>')
.addClass(targetId)
.prependTo(targetContainer)
.hide();
}
return button.appendTo(target);
}
function makeStatusDiv(anchor, statusClass) {
return $('<div class="alert method_table" role="alert" style="word-break: break-all"></div>')
.appendTo($(anchor).closest("table").find("thead th[colspan=3]").first())
.addClass(statusClass || 'alert-info')
.hide();
}
// SFTP - Identity show button
(function ($identityType) {
var identityDiv = makeStatusDiv($identityType);
makeButton("{{ lang._('Show Identity') }}", "upload_sftp", "btn-info")
.click(function () {
identityDiv.hide();
var button = $(this);
button.prop('disabled', true).find(".fa-spinner").show();
ajaxCall("/api/acmeclient/actions/sftpGetIdentity", getFormData("DialogAction").action, function (data, status) {
button.prop('disabled', false).find(".fa-spinner").hide();
if (status === "success" && data.status === "ok")
identityDiv.text(data.identity).show();
else
identityDiv.text("{{ lang._('Failed loading identity') }}").show();
});
});
// Hide when input changes that influences the identity.
$identityType.change(function() {
identityDiv.hide();
});
})($('#action\\.sftp_identity_type'));
// SFTP - Connection test button
(function ($user) {
var statusDiv = makeStatusDiv($user, 'alert-success').html(
'<div class="message"></div>'
+ '<div class="detail-enabler" style="cursor: pointer"><i class="fa fa-plus-square"></i></div>'
+ '<div class="detail" style="font-family: monospace"></div>');
statusDiv.find(".detail-enabler").click(function() {
$(".detail", statusDiv).show();
$(this).hide();
});
var errors = [
{cond: ["connect_failed", "invalid_parameters"], msg: "{{ lang._('Host or username not specified.') }}"},
{cond: ["connect_failed", "host_not_trusted"], msg: "{{ lang._('Host cannot be trusted.') }}"},
{cond: ["connect_failed", "host_not_resolved"], msg: "{{ lang._('Failed to resolve hostname.') }}"},
{cond: ["connect_failed", "connection_refused"], msg: "{{ lang._('Connection to host refused.') }}"},
{cond: ["connect_failed", "network_timeout"], msg: "{{ lang._('Connection timed out.') }}"},
{cond: ["connect_failed", "network_unreachable"], msg: "{{ lang._('Host not reachable.') }}"},
{cond: ["connect_failed", "permission_denied"], msg: "{{ lang._('Host does not permit a connection for the specified user & identity.') }}"},
{cond: ["connect_failed"], msg: "{{ lang._('Failed to connect to host.') }}"},
{cond: ["change_home_dir_failed"], msg: "{{ lang._('Failed to change the remote path.') }}"},
{cond: ["permission_denied"], msg: "{{ lang._('Uploads are not allowed to the specified remote path.') }}"},
{msg: "{{ lang._('Test failed, see details.') }}"},
];
makeButton("{{ lang._('Test Connection') }}", "upload_sftp")
.click(function () {
statusDiv.hide();
var button = $(this);
button.prop('disabled', true).find(".fa-spinner").show();
ajaxCall("/api/acmeclient/actions/sftpTestConnection", getFormData("DialogAction").action, function (data, status) {
button.prop('disabled', false).find(".fa-spinner").hide();
var message = "", detail = "", statusClass = "alert-warning";
if (status === "success") {
if (data.success === true) {
statusClass = "alert-success";
message = "{{ lang._('Connection and upload test succeeded.') }}"
} else {
detail = JSON.stringify(data, null, ' ');
for (var i = 0; i < errors.length; i++) {
var error = errors[i],
matching = (error.cond || []).filter(function (condition) {
return data[condition] === true;
});
if (matching.length === error.cond.length) {
message = error.msg;
break;
}
}
}
} else {
message = "{{ lang._('Test not possible. Failed to talk to firewall backend.') }}";
}
$(".message", statusDiv).html(message);
$(".detail", statusDiv).text(detail).hide();
$(".detail-enabler", statusDiv).toggle(detail !== "");
statusDiv.removeClass("alert-success alert-warning").addClass(statusClass).show();
});
});
})($('#action\\.sftp_user'));
// Eagerly hiding method tables to avoid contents popping up when opening the dialog for the first time.
$(".method_table").hide();
});
</script>
@@ -1290,6 +1290,9 @@ function run_restart_actions($certlist, $modelObj)
case 'upload_highwinds':
$response = $backend->configdRun("acmeclient upload_highwinds ${cert_id} ${action_id}");
break;
case 'upload_sftp':
$response = $backend->configdRun("acmeclient upload-sftp ${cert_id} ${action_id}");
break;
case 'configd':
// Make sure a configd command was specified.
if (empty((string)$action->configd)) {
File diff suppressed because it is too large Load Diff
@@ -84,6 +84,24 @@ parameters:-c %s -a %s
type:script
message:uploading a certificate to highwinds
[upload-sftp]
command:/usr/local/opnsense/scripts/OPNsense/AcmeClient/upload_sftp.php
parameters:--certificates=%s --automation-id=%s
type:script
message:uploading a certificate to sftp server
[test-sftp-connection]
command:/usr/local/opnsense/scripts/OPNsense/AcmeClient/upload_sftp.php
parameters:--host=%s --host-key=%s --port=%s --user=%s --identity-type=%s --remote-path=%s --no-error test-connection
type:script_output
message:testing connection to sftp server
[show-sftp-identity]
command:/usr/local/opnsense/scripts/OPNsense/AcmeClient/upload_sftp.php
parameters:--identity-type=%s --host=%s show-identity
type:script_output
message:prints the public key used to connect to sftp server
[reset-acme-client]
command:/usr/bin/find /var/etc/acme-client/home /var/etc/acme-client/configs /var/etc/acme-client/certs /var/etc/acme-client/keys /var/etc/acme-client/accounts -type f -delete
parameters: