From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by lists.proxmox.com (Postfix) with ESMTPS id 8D65E6A7A0 for ; Mon, 15 Mar 2021 12:07:37 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 81E5A1FCD2 for ; Mon, 15 Mar 2021 12:07:37 +0100 (CET) Received: from proxmox-new.maurer-it.com (proxmox-new.maurer-it.com [212.186.127.180]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256) (No client certificate requested) by firstgate.proxmox.com (Proxmox) with ESMTPS id 61EFE1FCC7 for ; Mon, 15 Mar 2021 12:07:35 +0100 (CET) Received: from proxmox-new.maurer-it.com (localhost.localdomain [127.0.0.1]) by proxmox-new.maurer-it.com (Proxmox) with ESMTP id 2EA6442077 for ; Mon, 15 Mar 2021 12:07:35 +0100 (CET) Date: Mon, 15 Mar 2021 12:07:25 +0100 From: Fabian =?iso-8859-1?q?Gr=FCnbichler?= To: pmg-devel@lists.proxmox.com, Wolfgang Bumiller References: <20210312152421.30114-1-w.bumiller@proxmox.com> <20210312152421.30114-6-w.bumiller@proxmox.com> In-Reply-To: <20210312152421.30114-6-w.bumiller@proxmox.com> MIME-Version: 1.0 User-Agent: astroid/0.15.0 (https://github.com/astroidmail/astroid) Message-Id: <1615805399.4r6fcjaqah.astroid@nora.none> Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: quoted-printable X-SPAM-LEVEL: Spam detection results: 0 AWL 0.027 Adjusted score from AWL reputation of From: address KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment RCVD_IN_DNSWL_MED -2.3 Sender listed at https://www.dnswl.org/, medium trust SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Subject: Re: [pmg-devel] [PATCH v2 api 5/8] api: add ACME and ACMEPlugin module X-BeenThere: pmg-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox Mail Gateway development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-List-Received-Date: Mon, 15 Mar 2021 11:07:37 -0000 one permission-related comment and some nits inline On March 12, 2021 4:23 pm, Wolfgang Bumiller wrote: > This adds the cluster-wide acme account and plugin > configuration: >=20 > * /config/acme > |`+ account/ > | '- {name} > |`- tos > |`- directories > |`- challenge-schema > `+ plugins/ > '- {name} >=20 > Signed-off-by: Wolfgang Bumiller > --- > Changes since v1: > * ACME account listing is now restricted to admin & audit > * Register/Update/Deactivate acme account limited to admin > * lock-call cleanups > * removed useless sort call >=20 > src/Makefile | 2 + > src/PMG/API2/ACME.pm | 437 +++++++++++++++++++++++++++++++++++++ > src/PMG/API2/ACMEPlugin.pm | 270 +++++++++++++++++++++++ > src/PMG/API2/Config.pm | 7 + > 4 files changed, 716 insertions(+) > create mode 100644 src/PMG/API2/ACME.pm > create mode 100644 src/PMG/API2/ACMEPlugin.pm >=20 > diff --git a/src/Makefile b/src/Makefile > index ce76f9f..ebc6bd8 100644 > --- a/src/Makefile > +++ b/src/Makefile > @@ -155,6 +155,8 @@ LIBSOURCES =3D \ > PMG/API2/When.pm \ > PMG/API2/What.pm \ > PMG/API2/Action.pm \ > + PMG/API2/ACME.pm \ > + PMG/API2/ACMEPlugin.pm \ > PMG/API2.pm \ > =20 > SOURCES =3D ${LIBSOURCES} ${CLI_BINARIES} ${TEMPLATES_FILES} ${CONF_MANS= } ${CLI_MANS} ${SERVICE_MANS} ${SERVICE_UNITS} ${TIMER_UNITS} pmg-sources.l= ist pmg-apt.conf pmg-initramfs.conf > diff --git a/src/PMG/API2/ACME.pm b/src/PMG/API2/ACME.pm > new file mode 100644 > index 0000000..cf6a5e8 > --- /dev/null > +++ b/src/PMG/API2/ACME.pm > @@ -0,0 +1,437 @@ > +package PMG::API2::ACME; > + > +use strict; > +use warnings; > + > +use PVE::Exception qw(raise_param_exc); > +use PVE::JSONSchema qw(get_standard_option); > +use PVE::Tools qw(extract_param); > + > +use PVE::ACME::Challenge; > + > +use PMG::RESTEnvironment; > +use PMG::RS::Acme; > +use PMG::CertHelpers; > + > +use PMG::API2::ACMEPlugin; > + > +use base qw(PVE::RESTHandler); > + > +__PACKAGE__->register_method ({ > + subclass =3D> "PMG::API2::ACMEPlugin", > + path =3D> 'plugins', > +}); > + > +# FIXME: Put this list in pve-common or proxmox-acme{,-rs}? > +my $acme_directories =3D [ > + { > + name =3D> 'Let\'s Encrypt V2', > + url =3D> 'https://acme-v02.api.letsencrypt.org/directory', > + }, > + { > + name =3D> 'Let\'s Encrypt V2 Staging', > + url =3D> 'https://acme-staging-v02.api.letsencrypt.org/directory', > + }, > +]; > +my $acme_default_directory_url =3D $acme_directories->[0]->{url}; > +my $account_contact_from_param =3D sub { > + my @addresses =3D PVE::Tools::split_list(extract_param($_[0], 'conta= ct')); > + return [ map { "mailto:$_" } @addresses ]; > +}; > +my $acme_account_dir =3D PMG::CertHelpers::acme_account_dir(); > + > +__PACKAGE__->register_method ({ > + name =3D> 'index', > + path =3D> '', > + method =3D> 'GET', > + permissions =3D> { user =3D> 'all' }, > + description =3D> "ACME index.", > + parameters =3D> { > + additionalProperties =3D> 0, > + properties =3D> { > + }, > + }, > + returns =3D> { > + type =3D> 'array', > + items =3D> { > + type =3D> "object", > + properties =3D> {}, > + }, > + links =3D> [ { rel =3D> 'child', href =3D> "{name}" } ], > + }, > + code =3D> sub { > + my ($param) =3D @_; > + > + return [ > + { name =3D> 'account' }, > + { name =3D> 'tos' }, > + { name =3D> 'directories' }, > + { name =3D> 'plugins' }, > + { name =3D> 'challengeschema' }, > + ]; > + }}); > + > +__PACKAGE__->register_method ({ > + name =3D> 'account_index', > + path =3D> 'account', > + method =3D> 'GET', > + permissions =3D> { check =3D> [ 'admin', 'audit' ] }, > + description =3D> "ACME account index.", > + protected =3D> 1, > + parameters =3D> { > + additionalProperties =3D> 0, > + properties =3D> { > + }, > + }, > + returns =3D> { > + type =3D> 'array', > + items =3D> { > + type =3D> "object", > + properties =3D> {}, > + }, > + links =3D> [ { rel =3D> 'child', href =3D> "{name}" } ], > + }, > + code =3D> sub { > + my ($param) =3D @_; > + > + my $accounts =3D PMG::CertHelpers::list_acme_accounts(); > + return [ map { { name =3D> $_ } } @$accounts ]; > + }}); > + > +__PACKAGE__->register_method ({ > + name =3D> 'register_account', > + path =3D> 'account', > + method =3D> 'POST', > + description =3D> "Register a new ACME account with CA.", > + proxyto =3D> 'master', > + permissions =3D> { check =3D> [ 'admin' ] }, > + protected =3D> 1, > + parameters =3D> { > + additionalProperties =3D> 0, > + properties =3D> { > + name =3D> get_standard_option('pmg-acme-account-name'), > + contact =3D> get_standard_option('pmg-acme-account-contact'), > + tos_url =3D> { > + type =3D> 'string', > + description =3D> 'URL of CA TermsOfService - setting this indicates ag= reement.', > + optional =3D> 1, > + }, > + directory =3D> get_standard_option('pmg-acme-directory-url', { > + default =3D> $acme_default_directory_url, > + optional =3D> 1, > + }), > + }, > + }, > + returns =3D> { > + type =3D> 'string', > + }, > + code =3D> sub { > + my ($param) =3D @_; > + > + my $rpcenv =3D PMG::RESTEnvironment->get(); > + my $authuser =3D $rpcenv->get_user(); > + > + my $account_name =3D extract_param($param, 'name') // 'default'; > + my $account_file =3D "${acme_account_dir}/${account_name}"; > + mkdir $acme_account_dir if ! -e $acme_account_dir; > + > + raise_param_exc({'name' =3D> "ACME account config file '${account_name}= ' already exists."}) > + if -e $account_file; > + > + my $directory =3D extract_param($param, 'directory') // $acme_default_d= irectory_url; > + my $contact =3D $account_contact_from_param->($param); > + > + my $realcmd =3D sub { > + PMG::CertHelpers::lock_acme($account_name, 10, sub { > + die "ACME account config file '${account_name}' already exists.\n" > + if -e $account_file; > + > + print "Registering new ACME account..\n"; > + my $acme =3D PMG::RS::Acme->new($directory); > + eval { > + $acme->new_account($account_file, defined($param->{tos_url}), $con= tact, undef); > + }; > + if (my $err =3D $@) { > + unlink $account_file; > + die "Registration failed: $err\n"; > + } > + my $location =3D $acme->location(); > + print "Registration successful, account URL: '$location'\n"; > + }); > + }; > + > + return $rpcenv->fork_worker('acmeregister', undef, $authuser, $realcmd)= ; > + }}); > + > +my $update_account =3D sub { > + my ($param, $msg, %info) =3D @_; > + > + my $account_name =3D extract_param($param, 'name') // 'default'; > + my $account_file =3D "${acme_account_dir}/${account_name}"; might warrant a short helper, repeated a few times here and in the next=20 patch. just to have the path mapping in a single location in case we=20 ever want to adapt it. > + > + raise_param_exc({'name' =3D> "ACME account config file '${account_na= me}' does not exist."}) > + if ! -e $account_file; > + > + > + my $rpcenv =3D PMG::RESTEnvironment->get(); > + my $authuser =3D $rpcenv->get_user(); > + > + my $realcmd =3D sub { > + PMG::CertHelpers::lock_acme($account_name, 10, sub { > + die "ACME account config file '${account_name}' does not exist.\n" > + if ! -e $account_file; this could be skipped > + > + my $acme =3D PMG::RS::Acme->load($account_file); since this should die with a nice error message anyhow? possibly wrapped=20 in an eval to add the account name as prefix to the error message, but=20 the file name is a direct mapping anyhow.. this is repeated a few times down below and in the next patch > + $acme->update_account(\%info); > + if ($info{status} && $info{status} eq 'deactivated') { > + my $deactivated_name; > + for my $i (0..100) { > + my $candidate =3D "${acme_account_dir}/_deactivated_${account_name= }_${i}"; > + if (! -e $candidate) { > + $deactivated_name =3D $candidate; > + last; > + } > + } > + if ($deactivated_name) { > + print "Renaming account file from '$account_file' to '$deactivated= _name'\n"; > + rename($account_file, $deactivated_name) or > + warn ".. failed - $!\n"; > + } else { > + warn "No free slot to rename deactivated account file '$account_fi= le', leaving in place\n"; > + } > + } > + }); > + }; > + > + return $rpcenv->fork_worker("acme${msg}", undef, $authuser, $realcmd= ); > +}; > + > +__PACKAGE__->register_method ({ > + name =3D> 'update_account', > + path =3D> 'account/{name}', > + method =3D> 'PUT', > + description =3D> "Update existing ACME account information with CA. = Note: not specifying any new account information triggers a refresh.", > + proxyto =3D> 'master', > + permissions =3D> { check =3D> [ 'admin' ] }, > + protected =3D> 1, > + parameters =3D> { > + additionalProperties =3D> 0, > + properties =3D> { > + name =3D> get_standard_option('pmg-acme-account-name'), > + contact =3D> get_standard_option('pmg-acme-account-contact', { > + optional =3D> 1, > + }), > + }, > + }, > + returns =3D> { > + type =3D> 'string', > + }, > + code =3D> sub { > + my ($param) =3D @_; > + > + my $contact =3D $account_contact_from_param->($param); > + if (scalar @$contact) { > + return $update_account->($param, 'update', contact =3D> $contact); > + } else { > + return $update_account->($param, 'refresh'); > + } > + }}); > + > +__PACKAGE__->register_method ({ > + name =3D> 'get_account', > + path =3D> 'account/{name}', > + method =3D> 'GET', > + description =3D> "Return existing ACME account information.", > + protected =3D> 1, > + proxyto =3D> 'master', > + parameters =3D> { > + additionalProperties =3D> 0, > + properties =3D> { > + name =3D> get_standard_option('pmg-acme-account-name'), > + }, > + }, > + returns =3D> { > + type =3D> 'object', > + additionalProperties =3D> 0, > + properties =3D> { > + account =3D> { > + type =3D> 'object', > + optional =3D> 1, > + renderer =3D> 'yaml', > + }, > + directory =3D> get_standard_option('pmg-acme-directory-url', { > + optional =3D> 1, > + }), > + location =3D> { > + type =3D> 'string', > + optional =3D> 1, > + }, > + tos =3D> { > + type =3D> 'string', > + optional =3D> 1, > + }, > + }, > + }, > + code =3D> sub { > + my ($param) =3D @_; > + > + my $account_name =3D extract_param($param, 'name') // 'default'; > + my $account_file =3D "${acme_account_dir}/${account_name}"; see above > + > + raise_param_exc({'name' =3D> "ACME account config file '${account_name}= ' does not exist."}) > + if ! -e $account_file; > + > + my $acme =3D PMG::RS::Acme->load($account_file); see above > + my $data =3D $acme->account(); > + > + return { > + account =3D> $data->{account}, > + tos =3D> $data->{tos}, > + location =3D> $data->{location}, > + directory =3D> $data->{directoryUrl}, > + }; > + }}); > + > +__PACKAGE__->register_method ({ > + name =3D> 'deactivate_account', > + path =3D> 'account/{name}', > + method =3D> 'DELETE', > + description =3D> "Deactivate existing ACME account at CA.", > + protected =3D> 1, > + proxyto =3D> 'master', > + permissions =3D> { check =3D> [ 'admin' ] }, > + parameters =3D> { > + additionalProperties =3D> 0, > + properties =3D> { > + name =3D> get_standard_option('pmg-acme-account-name'), > + }, > + }, > + returns =3D> { > + type =3D> 'string', > + }, > + code =3D> sub { > + my ($param) =3D @_; > + > + return $update_account->($param, 'deactivate', status =3D> 'deactivated= '); > + }}); > + > +__PACKAGE__->register_method ({ > + name =3D> 'get_tos', > + path =3D> 'tos', > + method =3D> 'GET', > + description =3D> "Retrieve ACME TermsOfService URL from CA.", > + permissions =3D> { user =3D> 'all' }, > + parameters =3D> { > + additionalProperties =3D> 0, > + properties =3D> { > + directory =3D> get_standard_option('pmg-acme-directory-url', { > + default =3D> $acme_default_directory_url, > + optional =3D> 1, > + }), > + }, > + }, > + returns =3D> { > + type =3D> 'string', > + optional =3D> 1, > + description =3D> 'ACME TermsOfService URL.', > + }, > + code =3D> sub { > + my ($param) =3D @_; > + > + my $directory =3D extract_param($param, 'directory') // $acme_default_d= irectory_url; > + > + my $acme =3D PMG::RS::Acme->new($directory); > + my $meta =3D $acme->get_meta(); > + > + return $meta ? $meta->{termsOfService} : undef; > + }}); > + > +__PACKAGE__->register_method ({ > + name =3D> 'get_directories', > + path =3D> 'directories', > + method =3D> 'GET', > + description =3D> "Get named known ACME directory endpoints.", > + permissions =3D> { user =3D> 'all' }, > + parameters =3D> { > + additionalProperties =3D> 0, > + properties =3D> {}, > + }, > + returns =3D> { > + type =3D> 'array', > + items =3D> { > + type =3D> 'object', > + additionalProperties =3D> 0, > + properties =3D> { > + name =3D> { > + type =3D> 'string', > + }, > + url =3D> get_standard_option('pmg-acme-directory-url'), > + }, > + }, > + }, > + code =3D> sub { > + my ($param) =3D @_; > + > + return $acme_directories; > + }}); > + > +__PACKAGE__->register_method ({ > + name =3D> 'challengeschema', > + path =3D> 'challenge-schema', > + method =3D> 'GET', > + description =3D> "Get schema of ACME challenge types.", > + permissions =3D> { user =3D> 'all' }, > + parameters =3D> { > + additionalProperties =3D> 0, > + properties =3D> {}, > + }, > + returns =3D> { > + type =3D> 'array', > + items =3D> { > + type =3D> 'object', > + additionalProperties =3D> 0, > + properties =3D> { > + id =3D> { > + type =3D> 'string', > + }, > + name =3D> { > + description =3D> 'Human readable name, falls back to id', > + type =3D> 'string', > + }, > + type =3D> { > + type =3D> 'string', > + }, > + schema =3D> { > + type =3D> 'object', > + }, > + }, > + }, > + }, > + code =3D> sub { > + my ($param) =3D @_; > + > + my $plugin_type_enum =3D PVE::ACME::Challenge->lookup_types(); > + > + my $res =3D []; > + > + for my $type (@$plugin_type_enum) { > + my $plugin =3D PVE::ACME::Challenge->lookup($type); > + next if !$plugin->can('get_supported_plugins'); > + > + my $plugin_type =3D $plugin->type(); > + my $plugins =3D $plugin->get_supported_plugins(); > + for my $id (sort keys %$plugins) { > + my $schema =3D $plugins->{$id}; > + push @$res, { > + id =3D> $id, > + name =3D> $schema->{name} // $id, > + type =3D> $plugin_type, > + schema =3D> $schema, > + }; > + } > + } > + > + return $res; > + }}); > + > +1; > diff --git a/src/PMG/API2/ACMEPlugin.pm b/src/PMG/API2/ACMEPlugin.pm > new file mode 100644 > index 0000000..7ab6e59 > --- /dev/null > +++ b/src/PMG/API2/ACMEPlugin.pm > @@ -0,0 +1,270 @@ > +package PMG::API2::ACMEPlugin; > + > +use strict; > +use warnings; > + > +use Storable qw(dclone); > + > +use PVE::ACME::Challenge; > +use PVE::ACME::DNSChallenge; > +use PVE::ACME::StandAlone; > +use PVE::INotify; > +use PVE::JSONSchema qw(get_standard_option); > +use PVE::Tools qw(extract_param); > + > +use base qw(PVE::RESTHandler); > + > +my $inotify_file_id =3D 'pmg-acme-plugins-config.conf'; > +my $config_filename =3D '/etc/pmg/acme-plugins.conf'; > +my $lockfile =3D "/var/lock/pmg-acme-plugins-config.lck"; > + > +PVE::ACME::DNSChallenge->register(); > +PVE::ACME::StandAlone->register(); > +PVE::ACME::Challenge->init(); > + > +PVE::JSONSchema::register_standard_option('pmg-acme-pluginid', { > + type =3D> 'string', > + format =3D> 'pve-configid', > + description =3D> 'Unique identifier for ACME plugin instance.', > +}); > + > +sub read_pmg_acme_challenge_config { > + my ($filename, $fh) =3D @_; > + local $/ =3D undef; # slurp mode > + my $raw =3D defined($fh) ? <$fh> : ''; > + return PVE::ACME::Challenge->parse_config($filename, $raw); > +} > + > +sub write_pmg_acme_challenge_config { > + my ($filename, $fh, $cfg) =3D @_; > + my $raw =3D PVE::ACME::Challenge->write_config($filename, $cfg); > + PVE::Tools::safe_print($filename, $fh, $raw); > +} > + > +PVE::INotify::register_file($inotify_file_id, $config_filename, > + \&read_pmg_acme_challenge_config, > + \&write_pmg_acme_challenge_config, > + undef, > + always_call_parser =3D> 1); > + > +sub lock_config { > + my ($code) =3D @_; > + my $p =3D PVE::Tools::lock_file($lockfile, undef, $code); > + die $@ if $@; > + return $p; > +} > + > +sub load_config { > + # auto-adds the standalone plugin if no config is there for backward= s > + # compatibility, so ALWAYS call the cfs registered parser > + return PVE::INotify::read_file($inotify_file_id); > +} > + > +sub write_config { > + my ($self) =3D @_; > + return PVE::INotify::write_file($inotify_file_id, $self); > +} > + > +my $plugin_type_enum =3D PVE::ACME::Challenge->lookup_types(); > + > +my $modify_cfg_for_api =3D sub { > + my ($cfg, $pluginid) =3D @_; > + > + die "ACME plugin '$pluginid' not defined\n" if !defined($cfg->{ids}-= >{$pluginid}); > + > + my $plugin_cfg =3D dclone($cfg->{ids}->{$pluginid}); > + $plugin_cfg->{plugin} =3D $pluginid; > + $plugin_cfg->{digest} =3D $cfg->{digest}; > + > + return $plugin_cfg; > +}; > + > +__PACKAGE__->register_method ({ > + name =3D> 'index', > + path =3D> '', > + method =3D> 'GET', > + permissions =3D> { check =3D> [ 'admin', 'audit' ] }, audit seems a bit 'weak' since the plugin config will almost certainly=20 contain some form of API access tokens for DNS plugins.. e.g., compare=20 with PVE where this has 'Sys.Modify' on / as requirement. alternatively, 'audit'-only could return just the fact that the plugin=20 exists, but not the config (or a censored config with just the keys, and=20 not the actual values). has a slight danger in a rather contrived=20 scenario where the config is requested using audit, and then updated=20 based on the result using an admin access though. > + description =3D> "ACME plugin index.", > + protected =3D> 1, > + parameters =3D> { > + additionalProperties =3D> 0, > + properties =3D> { > + type =3D> { > + description =3D> "Only list ACME plugins of a specific type", > + type =3D> 'string', > + enum =3D> $plugin_type_enum, > + optional =3D> 1, > + }, > + }, > + }, > + returns =3D> { > + type =3D> 'array', > + items =3D> { > + type =3D> "object", > + properties =3D> { > + plugin =3D> get_standard_option('pmg-acme-pluginid'), > + }, > + }, > + links =3D> [ { rel =3D> 'child', href =3D> "{plugin}" } ], > + }, > + code =3D> sub { > + my ($param) =3D @_; > + > + my $cfg =3D load_config(); > + > + my $res =3D []; > + foreach my $pluginid (keys %{$cfg->{ids}}) { > + my $plugin_cfg =3D $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 =3D> 'get_plugin_config', > + path =3D> '{id}', > + method =3D> 'GET', > + description =3D> "Get ACME plugin configuration.", > + permissions =3D> { check =3D> [ 'admin', 'audit' ] }, see index API endpoint - same applies here > + protected =3D> 1, > + parameters =3D> { > + additionalProperties =3D> 0, > + properties =3D> { > + id =3D> get_standard_option('pmg-acme-pluginid'), > + }, > + }, > + returns =3D> { > + type =3D> 'object', > + }, > + code =3D> sub { > + my ($param) =3D @_; > + > + my $cfg =3D load_config(); > + return $modify_cfg_for_api->($cfg, $param->{id}); > + } > +}); > + > +__PACKAGE__->register_method({ > + name =3D> 'add_plugin', > + path =3D> '', > + method =3D> 'POST', > + description =3D> "Add ACME plugin configuration.", > + permissions =3D> { check =3D> [ 'admin' ] }, > + protected =3D> 1, > + parameters =3D> PVE::ACME::Challenge->createSchema(), > + returns =3D> { > + type =3D> "null" > + }, > + code =3D> sub { > + my ($param) =3D @_; > + > + my $id =3D extract_param($param, 'id'); > + my $type =3D extract_param($param, 'type'); > + > + lock_config(sub { > + my $cfg =3D load_config(); > + die "ACME plugin ID '$id' already exists\n" if defined($cfg->{ids}-= >{$id}); > + > + my $plugin =3D PVE::ACME::Challenge->lookup($type); > + my $opts =3D $plugin->check_config($id, $param, 1, 1); > + > + $cfg->{ids}->{$id} =3D $opts; > + $cfg->{ids}->{$id}->{type} =3D $type; > + > + write_config($cfg); > + }); > + die "$@" if $@; > + > + return undef; > + } > +}); > + > +__PACKAGE__->register_method({ > + name =3D> 'update_plugin', > + path =3D> '{id}', > + method =3D> 'PUT', > + description =3D> "Update ACME plugin configuration.", > + permissions =3D> { check =3D> [ 'admin' ] }, > + protected =3D> 1, > + parameters =3D> PVE::ACME::Challenge->updateSchema(), > + returns =3D> { > + type =3D> "null" > + }, > + code =3D> sub { > + my ($param) =3D @_; > + > + my $id =3D extract_param($param, 'id'); > + my $delete =3D extract_param($param, 'delete'); > + my $digest =3D extract_param($param, 'digest'); > + > + lock_config(sub { > + my $cfg =3D load_config(); > + PVE::Tools::assert_if_modified($cfg->{digest}, $digest); > + my $plugin_cfg =3D $cfg->{ids}->{$id}; > + die "ACME plugin ID '$id' does not exist\n" if !$plugin_cfg; > + > + my $type =3D $plugin_cfg->{type}; > + my $plugin =3D PVE::ACME::Challenge->lookup($type); > + > + if (defined($delete)) { > + my $schema =3D $plugin->private(); > + my $options =3D $schema->{options}->{$type}; > + for my $k (PVE::Tools::split_list($delete)) { > + my $d =3D $options->{$k} || die "no such option '$k'\n"; > + die "unable to delete required option '$k'\n" if !$d->{optional}; > + > + delete $cfg->{ids}->{$id}->{$k}; > + } > + } > + > + my $opts =3D $plugin->check_config($id, $param, 0, 1); > + for my $k (keys %$opts) { > + $plugin_cfg->{$k} =3D $opts->{$k}; > + } > + > + write_config($cfg); > + }); > + die "$@" if $@; > + > + return undef; > + } > +}); > + > +__PACKAGE__->register_method({ > + name =3D> 'delete_plugin', > + path =3D> '{id}', > + method =3D> 'DELETE', > + description =3D> "Delete ACME plugin configuration.", > + permissions =3D> { check =3D> [ 'admin' ] }, > + protected =3D> 1, > + parameters =3D> { > + additionalProperties =3D> 0, > + properties =3D> { > + id =3D> get_standard_option('pmg-acme-pluginid'), > + }, > + }, > + returns =3D> { > + type =3D> "null" > + }, > + code =3D> sub { > + my ($param) =3D @_; > + > + my $id =3D extract_param($param, 'id'); > + > + lock_config(sub { > + my $cfg =3D load_config(); > + > + delete $cfg->{ids}->{$id}; > + > + write_config($cfg); > + }); > + die "$@" if $@; > + > + return undef; > + } > +}); > + > +1; > diff --git a/src/PMG/API2/Config.pm b/src/PMG/API2/Config.pm > index e11eb3f..c5697e1 100644 > --- a/src/PMG/API2/Config.pm > +++ b/src/PMG/API2/Config.pm > @@ -26,6 +26,7 @@ use PMG::API2::DestinationTLSPolicy; > use PMG::API2::DKIMSign; > use PMG::API2::SACustom; > use PMG::API2::PBS::Remote; > +use PMG::API2::ACME; > =20 > use base qw(PVE::RESTHandler); > =20 > @@ -99,6 +100,11 @@ __PACKAGE__->register_method ({ > path =3D> 'pbs', > }); > =20 > +__PACKAGE__->register_method ({ > + subclass =3D> "PMG::API2::ACME", > + path =3D> 'acme', > +}); > + > __PACKAGE__->register_method ({ > name =3D> 'index',=20 > path =3D> '', > @@ -138,6 +144,7 @@ __PACKAGE__->register_method ({ > push @$res, { section =3D> 'tlspolicy' }; > push @$res, { section =3D> 'dkim' }; > push @$res, { section =3D> 'pbs' }; > + push @$res, { section =3D> 'acme' }; > =20 > return $res; > }}); > --=20 > 2.20.1 >=20 >=20 >=20 > _______________________________________________ > pmg-devel mailing list > pmg-devel@lists.proxmox.com > https://lists.proxmox.com/cgi-bin/mailman/listinfo/pmg-devel >=20 >=20 >=20 =