From 838470846c74c904b719efa209d954a92a26a1c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Gr=C3=BCnbichler?= Date: Mon, 20 Apr 2020 23:08:41 +0200 Subject: [PATCH] acme plugins: improve API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit add checks, encoding of loaded data files, update API path, proper inclusion into API tree Signed-off-by: Fabian Grünbichler --- PVE/API2/ACMEAccount.pm | 8 ++ PVE/API2/ACMEPlugin.pm | 163 +++++++++++++++++++++++++++++++++++----- PVE/API2/Cluster.pm | 5 -- PVE/CLI/pvenode.pm | 39 +++++----- 4 files changed, 172 insertions(+), 43 deletions(-) diff --git a/PVE/API2/ACMEAccount.pm b/PVE/API2/ACMEAccount.pm index 29d5dcf3..28f39b3b 100644 --- a/PVE/API2/ACMEAccount.pm +++ b/PVE/API2/ACMEAccount.pm @@ -10,6 +10,13 @@ use PVE::JSONSchema qw(get_standard_option); use PVE::RPCEnvironment; use PVE::Tools qw(extract_param); +use PVE::API2::ACMEPlugin; + +__PACKAGE__->register_method ({ + subclass => "PVE::API2::ACMEPlugin", + path => 'plugins', +}); + use base qw(PVE::RESTHandler); my $acme_directories = [ @@ -58,6 +65,7 @@ __PACKAGE__->register_method ({ { name => 'account' }, { name => 'tos' }, { name => 'directories' }, + { name => 'plugins' }, ]; }}); diff --git a/PVE/API2/ACMEPlugin.pm b/PVE/API2/ACMEPlugin.pm index 0a2a0b76..4c87242f 100644 --- a/PVE/API2/ACMEPlugin.pm +++ b/PVE/API2/ACMEPlugin.pm @@ -6,9 +6,12 @@ use warnings; use PVE::ACME::Challenge; use PVE::ACME::DNSChallenge; use PVE::ACME::StandAlone; +use PVE::JSONSchema qw(register_standard_option get_standard_option); use PVE::Tools qw(extract_param); use PVE::Cluster qw(cfs_read_file cfs_write_file cfs_register_file); + use MIME::Base64; +use Storable qw(dclone); use base qw(PVE::RESTHandler); @@ -22,50 +25,151 @@ PVE::ACME::DNSChallenge->register(); PVE::ACME::StandAlone->register(); PVE::ACME::Challenge->init(); +PVE::JSONSchema::register_standard_option('pve-acme-pluginid', { + type => 'string', + format => 'pve-configid', + description => 'Unique identifier for ACME plugin instance.', +}); + +my $plugin_type_enum = PVE::ACME::Challenge->lookup_types(); + +my $modify_cfg_for_api = sub { + my ($cfg, $pluginid) = @_; + + die "ACME plugin '$pluginid' not defined\n" + if !defined($cfg->{ids}->{$pluginid}); + + my $plugin_cfg = dclone($cfg->{ids}->{$pluginid}); + $plugin_cfg->{plugin} = $pluginid; + $plugin_cfg->{digest} = $cfg->{digest}; + + return $plugin_cfg; +}; + +__PACKAGE__->register_method ({ + name => 'index', + path => '', + method => 'GET', + permissions => { + check => ['perm', '/', [ 'Sys.Modify' ]], + }, + description => "ACME plugin index.", + protected => 1, + parameters => { + additionalProperties => 0, + properties => { + type => { + description => "Only list storage of specific type", + type => 'string', + enum => $plugin_type_enum, + optional => 1, + }, + }, + }, + returns => { + type => 'array', + items => { + type => "object", + properties => { + plugin => get_standard_option('pve-acme-pluginid'), + }, + }, + links => [ { rel => 'child', href => "{plugin}" } ], + }, + code => sub { + my ($param) = @_; + + my $cfg = load_config(); + + my $res =[]; + + foreach my $pluginid (keys %{$cfg->{ids}}) { + my $plugin_cfg = $modify_cfg_for_api->($cfg, $pluginid); + next if $param->{type} && $param->{type} ne $plugin_cfg->{type}; + push @$res, $plugin_cfg; + } + + return $res; + + }}); + __PACKAGE__->register_method({ name => 'get_plugin_config', - path => 'plugin', + path => '{id}', method => 'GET', - description => "Get ACME DNS plugin configurations.", + description => "Get ACME DNS plugin configuration.", permissions => { - check => ['perm', '/', [ 'Sys.Modily' ]], + check => ['perm', '/', [ 'Sys.Modify' ]], }, protected => 1, parameters => { additionalProperties => 0, properties => { + id => get_standard_option('pve-acme-pluginid'), }, }, returns => { type => 'object', }, code => sub { + my ($param) = @_; - return load_config(); + return $modify_cfg_for_api->(load_config(), $param->{id}); }}); my $update_config = sub { my ($id, $op, $type, $param) = @_; - my $conf = load_config(); + my $cfg = load_config(); if ( $op eq "add" ) { die "Section with ID: $id already exists\n" - if defined($conf->{ids}->{$id}); + if defined($cfg->{ids}->{$id}); - $conf->{ids}->{$id} = $param; - $conf->{ids}->{$id}->{type} = $type; + my $plugin = PVE::ACME::Challenge->lookup($type); + my $opts = $plugin->check_config($id, $param, 1, 1); + + $cfg->{ids}->{$id} = $opts; + $cfg->{ids}->{$id}->{type} = $type; + } elsif ($op eq "update") { + die "Section with ID; $id does not exist\n" + if !defined($cfg->{ids}->{$id}); + + my $delete = extract_param($param, 'delete'); + + $type = $cfg->{ids}->{$id}->{type}; + my $plugin = PVE::ACME::Challenge->lookup($type); + my $opts = $plugin->check_config($id, $param, 0, 1); + if ($delete) { + my $options = $plugin->private()->{options}->{$type}; + foreach my $k (PVE::Tools::split_list($delete)) { + my $d = $options->{$k} || die "no such option '$k'\n"; + die "unable to delete required option '$k'\n" if !$d->{optional}; + die "unable to delete fixed option '$k'\n" if $d->{fixed}; + die "cannot set and delete property '$k' at the same time!\n" + if defined($opts->{$k}); + + delete $cfg->{ids}->{$id}->{$k}; + } + } + + for my $k (keys %$opts) { + print "$k: $opts->{$k}\n"; + $cfg->{ids}->{$id}->{$k} = $opts->{$k}; + } } elsif ($op eq "del") { - delete $conf->{ids}->{$id}; + delete $cfg->{ids}->{$id}; + } else { + die 'undefined config update operation\n' if !defined($op); + die "unknown config update operation '$op'\n"; } - - PVE::Cluster::cfs_write_file($FILENAME, $conf); + PVE::Cluster::cfs_write_file($FILENAME, $cfg); }; __PACKAGE__->register_method({ name => 'add_plugin', - path => 'plugin', + path => '', method => 'POST', description => "Add ACME DNS plugin configuration.", permissions => { @@ -86,9 +190,31 @@ __PACKAGE__->register_method({ return undef; }}); +__PACKAGE__->register_method({ + name => 'update_plugin', + path => '{id}', + method => 'PUT', + description => "Update ACME DNS plugin configuration.", + permissions => { + check => ['perm', '/', [ 'Sys.Modify' ]], + }, + protected => 1, + parameters => PVE::ACME::Challenge->updateSchema(), + returns => { type => "null" }, + code => sub { + my ($param) = @_; + + my $id = extract_param($param, 'id'); + + PVE::Cluster::cfs_lock_file($FILENAME, undef, $update_config, $id, "update", undef, $param); + die "$@" if $@; + + return undef; + }}); + __PACKAGE__->register_method({ name => 'delete_plugin', - path => 'plugin', + path => '{id}', method => 'DELETE', description => "Delete ACME DNS plugin configuration.", permissions => { @@ -96,13 +222,10 @@ __PACKAGE__->register_method({ }, protected => 1, parameters => { - additionalProperties => 0, - properties => { - id => { - description => "Plugin configuration name", - type => 'string', - }, - }, + additionalProperties => 0, + properties => { + id => get_standard_option('pve-acme-pluginid'), + }, }, returns => { type => "null" }, code => sub { diff --git a/PVE/API2/Cluster.pm b/PVE/API2/Cluster.pm index 0810da0a..76560fa4 100644 --- a/PVE/API2/Cluster.pm +++ b/PVE/API2/Cluster.pm @@ -67,11 +67,6 @@ __PACKAGE__->register_method ({ path => 'acme', }); -__PACKAGE__->register_method ({ - subclass => "PVE::API2::ACMEPlugin", - path => 'acmeplugin', -}); - __PACKAGE__->register_method ({ subclass => "PVE::API2::Cluster::Ceph", path => 'ceph', diff --git a/PVE/CLI/pvenode.pm b/PVE/CLI/pvenode.pm index fd706a91..0efdcd56 100644 --- a/PVE/CLI/pvenode.pm +++ b/PVE/CLI/pvenode.pm @@ -11,6 +11,7 @@ use PVE::API2::NodeConfig; use PVE::API2::Nodes; use PVE::API2::Tasks; +use PVE::ACME::Challenge; use PVE::CertHelpers; use PVE::Certificate; use PVE::Exception qw(raise_param_exc raise); @@ -41,13 +42,22 @@ my $upid_exit = sub { sub param_mapping { my ($name) = @_; + my $load_file_and_encode = sub { + my ($filename) = @_; + + return PVE::ACME::Challenge->encode_value('string', 'data', PVE::Tools::file_get_contents($filename)); + }; + my $mapping = { 'upload_custom_cert' => [ 'certificates', 'key', ], 'add_plugin' => [ - 'data', + ['data', $load_file_and_encode, "File with one key-value pair per line, will be base64url encode for storage in plugin config.", 0], + ], + 'update_plugin' => [ + ['data', $load_file_and_encode, "File with one key-value pair per line, will be base64url encode for storage in plugin config.", 0], ], }; @@ -212,24 +222,17 @@ our $cmddef = { revoke => [ 'PVE::API2::ACME', 'revoke_certificate', [], { node => $nodename }, $upid_exit ], }, plugin => { - get => [ 'PVE::API2::ACMEPlugin', 'get_plugin_config', [], {}, - sub { - my $conf = shift; - print "Name\tType\tStatus\tapi\tdata\n"; - foreach my $key (keys %{$conf->{ids}} ) { - my $type = $conf->{ids}->{$key}->{type}; - my $status = $conf->{ids}->{$key}->{disable} ? - "disabled" : "active"; - my $api = $conf->{ids}->{$key}->{api} ? - $conf->{ids}->{$key}->{api} : "none"; - my $data = $conf->{ids}->{$key}->{data} ? - $conf->{ids}->{$key}->{data} : "none"; - - print "$key\t$type\t$status\t$api\t$data\n"; - } - } ], + list => [ 'PVE::API2::ACMEPlugin', 'index', [], {}, sub { + my ($data, $schema, $options) = @_; + PVE::CLIFormatter::print_api_result($data, $schema, undef, $options); + }, $PVE::RESTHandler::standard_output_options ], + config => [ 'PVE::API2::ACMEPlugin', 'get_plugin_config', ['id'], {}, sub { + my ($data, $schema, $options) = @_; + PVE::CLIFormatter::print_api_result($data, $schema, undef, $options); + }, $PVE::RESTHandler::standard_output_options ], add => [ 'PVE::API2::ACMEPlugin', 'add_plugin', ['type', 'id'] ], - del => [ 'PVE::API2::ACMEPlugin', 'delete_plugin', ['id'] ], + set => [ 'PVE::API2::ACMEPlugin', 'update_plugin', ['id'] ], + remove => [ 'PVE::API2::ACMEPlugin', 'delete_plugin', ['id'] ], }, },