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 3C9626ACD8 for ; Mon, 15 Mar 2021 22:39:48 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 31F65276FA for ; Mon, 15 Mar 2021 22:39:48 +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)) (No client certificate requested) by firstgate.proxmox.com (Proxmox) with ESMTPS id ADBCC276EA for ; Mon, 15 Mar 2021 22:39:46 +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 7204D45640 for ; Mon, 15 Mar 2021 22:39:46 +0100 (CET) Date: Mon, 15 Mar 2021 22:39:44 +0100 From: Stoiko Ivanov To: Wolfgang Bumiller Cc: pmg-devel@lists.proxmox.com Message-ID: <20210315223944.08e0138b@rosa.proxmox.com> In-Reply-To: <20210312152421.30114-9-w.bumiller@proxmox.com> References: <20210312152421.30114-1-w.bumiller@proxmox.com> <20210312152421.30114-9-w.bumiller@proxmox.com> X-Mailer: Claws Mail 3.17.3 (GTK+ 2.24.32; x86_64-pc-linux-gnu) MIME-Version: 1.0 Content-Type: text/plain; charset=US-ASCII Content-Transfer-Encoding: 7bit X-SPAM-LEVEL: Spam detection results: 0 AWL 0.065 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 8/8] add acme and cert subcommands to pmgconfig 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 21:39:48 -0000 On Fri, 12 Mar 2021 16:23:57 +0100 Wolfgang Bumiller wrote: > Signed-off-by: Wolfgang Bumiller > --- > src/PMG/CLI/pmgconfig.pm | 178 +++++++++++++++++++++++++++++++++++++++ > 1 file changed, 178 insertions(+) > > diff --git a/src/PMG/CLI/pmgconfig.pm b/src/PMG/CLI/pmgconfig.pm > index 85edfa5..4f948cf 100644 > --- a/src/PMG/CLI/pmgconfig.pm > +++ b/src/PMG/CLI/pmgconfig.pm > @@ -5,10 +5,13 @@ use warnings; > use IO::File; > use Data::Dumper; > > +use Term::ReadLine; > + > use PVE::SafeSyslog; > use PVE::Tools qw(extract_param); > use PVE::INotify; > use PVE::CLIHandler; > +use PVE::JSONSchema qw(get_standard_option); > > use PMG::RESTEnvironment; > use PMG::RuleDB; > @@ -18,14 +21,52 @@ use PMG::LDAPConfig; > use PMG::LDAPSet; > use PMG::Config; > use PMG::Ticket; > + > +use PMG::API2::ACME; > +use PMG::API2::ACMEPlugin; > +use PMG::API2::Certificates; > use PMG::API2::DKIMSign; > > use base qw(PVE::CLIHandler); > > +my $nodename = PVE::INotify::nodename(); > + > sub setup_environment { > PMG::RESTEnvironment->setup_default_cli_env(); > } > > +my $upid_exit = sub { > + my $upid = shift; > + my $status = PVE::Tools::upid_read_status($upid); > + print "Task $status\n"; > + exit($status eq 'OK' ? 0 : -1); > +}; > + > +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', $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], > + ], > + }; > + > + return $mapping->{$name}; > +} > + > __PACKAGE__->register_method ({ > name => 'dump', > path => 'dump', > @@ -184,6 +225,84 @@ __PACKAGE__->register_method ({ > return undef; > }}); > > +__PACKAGE__->register_method({ > + name => 'acme_register', > + path => 'acme_register', > + method => 'POST', > + description => "Register a new ACME account with a compatible CA.", > + parameters => { > + additionalProperties => 0, > + properties => { > + name => get_standard_option('pmg-acme-account-name'), > + contact => get_standard_option('pmg-acme-account-contact'), > + directory => get_standard_option('pmg-acme-directory-url', { > + optional => 1, > + }), > + }, > + }, > + returns => { type => 'null' }, > + code => sub { > + my ($param) = @_; > + > + if (!$param->{directory}) { > + my $directories = PMG::API2::ACME->get_directories({}); > + print "Directory endpoints:\n"; > + my $i = 0; > + while ($i < @$directories) { > + print $i, ") ", $directories->[$i]->{name}, " (", $directories->[$i]->{url}, ")\n"; > + $i++; > + } > + print $i, ") Custom\n"; > + > + my $term = Term::ReadLine->new('pmgconfig'); > + my $get_dir_selection = sub { > + my $selection = $term->readline("Enter selection: "); > + if ($selection =~ /^(\d+)$/) { > + $selection = $1; > + if ($selection == $i) { > + $param->{directory} = $term->readline("Enter custom URL: "); > + return; > + } elsif ($selection < $i && $selection >= 0) { > + $param->{directory} = $directories->[$selection]->{url}; > + return; > + } > + } > + print "Invalid selection.\n"; > + }; > + > + my $attempts = 0; > + while (!$param->{directory}) { > + die "Aborting.\n" if $attempts > 3; > + $get_dir_selection->(); > + $attempts++; > + } > + } > + print "\nAttempting to fetch Terms of Service from '$param->{directory}'..\n"; > + my $tos = PMG::API2::ACME->get_tos({ directory => $param->{directory} }); > + if ($tos) { > + print "Terms of Service: $tos\n"; > + my $term = Term::ReadLine->new('pvenode'); > + my $agreed = $term->readline('Do you agree to the above terms? [y|N]: '); > + die "Cannot continue without agreeing to ToS, aborting.\n" > + if ($agreed !~ /^y$/i); > + > + $param->{tos_url} = $tos; > + } else { > + print "No Terms of Service found, proceeding.\n"; > + } > + print "\nAttempting to register account with '$param->{directory}'..\n"; > + > + $upid_exit->(PMG::API2::ACME->register_account($param)); > + }}); > + > +my $print_cert_info = sub { > + my ($schema, $cert, $options) = @_; > + > + my $order = [qw(filename fingerprint subject issuer notbefore notafter public-key-type public-key-bits san)]; > + PVE::CLIFormatter::print_api_result( > + $cert, $schema, $order, { %$options, noheader => 1, sort_key => 0 }); > +}; > + > our $cmddef = { > 'dump' => [ __PACKAGE__, 'dump', []], > sync => [ __PACKAGE__, 'sync', []], > @@ -198,6 +317,65 @@ our $cmddef = { > die "no dkim_selector configured\n" if !defined($res->{record}); > print "$res->{record}\n"; > }], > + > + cert => { > + info => [ 'PMG::API2::Certificates', 'info', [], { node => $nodename }, sub { > + my ($res, $schema, $options) = @_; > + > + if (!$options->{'output-format'} || $options->{'output-format'} eq 'text') { > + for my $cert (sort { $a->{filename} cmp $b->{filename} } @$res) { > + $print_cert_info->($schema->{items}, $cert, $options); > + } > + } else { > + PVE::CLIFormatter::print_api_result($res, $schema, undef, $options); > + } > + > + }, $PVE::RESTHandler::standard_output_options], > + set => [ 'PMG::API2::Certificates', 'upload_custom_cert', ['type', 'certificates', 'key'], { node => $nodename }, sub { > + my ($res, $schema, $options) = @_; > + $print_cert_info->($schema, $res, $options); > + }, $PVE::RESTHandler::standard_output_options], > + delete => [ 'PMG::API2::Certificates', 'remove_custom_cert', ['type', 'restart'], { node => $nodename } ], small nit: by adding 'restart' as fixed parameter here I get an uninitialized warning in JSONSchema - maybe just drop it? > + }, > + > + acme => { > + account => { > + list => [ 'PMG::API2::ACME', 'account_index', [], {}, sub { > + my ($res) = @_; > + for my $acc (@$res) { > + print "$acc->{name}\n"; > + } > + }], > + register => [ __PACKAGE__, 'acme_register', ['name', 'contact'], {}, $upid_exit ], > + deactivate => [ 'PMG::API2::ACME', 'deactivate_account', ['name'], {}, $upid_exit ], > + info => [ 'PMG::API2::ACME', 'get_account', ['name'], {}, sub { > + my ($data, $schema, $options) = @_; > + PVE::CLIFormatter::print_api_result($data, $schema, undef, $options); > + }, $PVE::RESTHandler::standard_output_options], > + update => [ 'PMG::API2::ACME', 'update_account', ['name'], {}, $upid_exit ], > + }, > + cert => { > + order => [ 'PMG::API2::Certificates', 'new_acme_cert', ['type'], { node => $nodename }, $upid_exit ], > + > + > + renew => [ 'PMG::API2::Certificates', 'renew_acme_cert', ['type'], { node => $nodename }, $upid_exit ], > + revoke => [ 'PMG::API2::Certificates', 'revoke_acme_cert', ['type'], { node => $nodename }, $upid_exit ], > + }, > + plugin => { > + list => [ 'PMG::API2::ACMEPlugin', 'index', [], {}, sub { > + my ($data, $schema, $options) = @_; > + PVE::CLIFormatter::print_api_result($data, $schema, undef, $options); > + }, $PVE::RESTHandler::standard_output_options ], > + config => [ 'PMG::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 => [ 'PMG::API2::ACMEPlugin', 'add_plugin', ['type', 'id'] ], > + set => [ 'PMG::API2::ACMEPlugin', 'update_plugin', ['id'] ], > + remove => [ 'PMG::API2::ACMEPlugin', 'delete_plugin', ['id'] ], > + }, > + > + }, > }; > >