[pve-devel] [PATCH v2 manager 3/5] add ACME account API endpoints

Thomas Lamprecht t.lamprecht at proxmox.com
Mon Apr 30 14:03:48 CEST 2018


On 4/19/18 2:01 PM, Fabian Gr├╝nbichler wrote:
> for registering, updating, refreshing and deactiving a PVE-managed ACME
> account, as well as for retrieving the (optional, but required if
> available) terms of service of the ACME API provider / CA.
> 
> Signed-off-by: Fabian Gr├╝nbichler <f.gruenbichler at proxmox.com>
> ---
>  PVE/API2/Makefile       |   1 +
>  PVE/API2/ACMEAccount.pm | 347 ++++++++++++++++++++++++++++++++++++++++++++++++
>  PVE/API2/Cluster.pm     |   7 +
>  3 files changed, 355 insertions(+)
>  create mode 100644 PVE/API2/ACMEAccount.pm
> 
> diff --git a/PVE/API2/Makefile b/PVE/API2/Makefile
> index 51b8b30a..d72ddd9b 100644
> --- a/PVE/API2/Makefile
> +++ b/PVE/API2/Makefile
> @@ -14,6 +14,7 @@ PERLSOURCE = 			\
>  	Pool.pm			\
>  	Tasks.pm		\
>  	Network.pm		\
> +	ACMEAccount.pm		\
>  	NodeConfig.pm		\
>  	Services.pm
>  
> diff --git a/PVE/API2/ACMEAccount.pm b/PVE/API2/ACMEAccount.pm
> new file mode 100644
> index 00000000..fe7619d8
> --- /dev/null
> +++ b/PVE/API2/ACMEAccount.pm
> @@ -0,0 +1,347 @@
> +package PVE::API2::ACMEAccount;
> +
> +use strict;
> +use warnings;
> +
> +use PVE::ACME;
> +use PVE::CertHelpers;
> +use PVE::Exception qw(raise_param_exc);
> +use PVE::JSONSchema qw(get_standard_option);
> +use PVE::RPCEnvironment;
> +use PVE::Tools qw(extract_param);
> +
> +use base qw(PVE::RESTHandler);
> +
> +my $acme_directories = [
> +    {
> +	name => 'Let\'s Encrypt V2',
> +	url => 'https://acme-v02.api.letsencrypt.org/directory',
> +    },
> +    {
> +	name => 'Let\'s Encrypt V2 Staging',
> +	url => 'https://acme-staging-v02.api.letsencrypt.org/directory',
> +    },

Hmm, maybe add an optional node field which the WebUI combobox can display,
e.g., with:
* 'production environment'
* 'test environment (no rate limits)'

respectively. Could reduce confusion for users which are not too familiar
with Let's Encrypt and indirectly explains why we expose it.

But this can be added later on, rest looks OK.



> +];
> +
> +my $acme_default_directory_url = $acme_directories->[0]->{url};
> +
> +my $account_contact_from_param = sub {
> +    my ($param) = @_;
> +    return [ map { "mailto:$_" } PVE::Tools::split_list(extract_param($param, 'contact')) ];
> +};
> +
> +my $acme_account_dir = PVE::CertHelpers::acme_account_dir();
> +
> +__PACKAGE__->register_method ({
> +    name => 'index',
> +    path => '',
> +    method => 'GET',
> +    permissions => { user => 'all' },
> +    description => "ACMEAccount index.",
> +    parameters => {
> +	additionalProperties => 0,
> +	properties => {
> +	},
> +    },
> +    returns => {
> +	type => 'array',
> +	items => {
> +	    type => "object",
> +	    properties => {},
> +	},
> +	links => [ { rel => 'child', href => "{name}" } ],
> +    },
> +    code => sub {
> +	my ($param) = @_;
> +
> +	return [
> +	    { name => 'account' },
> +	    { name => 'tos' },
> +	    { name => 'directories' },
> +	];
> +    }});
> +
> +__PACKAGE__->register_method ({
> +    name => 'account_index',
> +    path => 'account',
> +    method => 'GET',
> +    permissions => { user => 'all' },
> +    description => "ACMEAccount index.",
> +    protected => 1,
> +    parameters => {
> +	additionalProperties => 0,
> +	properties => {
> +	},
> +    },
> +    returns => {
> +	type => 'array',
> +	items => {
> +	    type => "object",
> +	    properties => {},
> +	},
> +	links => [ { rel => 'child', href => "{name}" } ],
> +    },
> +    code => sub {
> +	my ($param) = @_;
> +
> +	my $accounts = PVE::CertHelpers::list_acme_accounts();
> +	return [ map { { name => $_ }  } @$accounts ];
> +    }});
> +
> +__PACKAGE__->register_method ({
> +    name => 'register_account',
> +    path => 'account',
> +    method => 'POST',
> +    description => "Register a new ACME account with CA.",
> +    protected => 1,
> +    parameters => {
> +	additionalProperties => 0,
> +	properties => {
> +	    name => get_standard_option('pve-acme-account-name'),
> +	    contact => get_standard_option('pve-acme-account-contact'),
> +	    tos_url => {
> +		type => 'string',
> +		description => 'URL of CA TermsOfService - setting this indicates agreement.',
> +		optional => 1,
> +	    },
> +	    directory => get_standard_option('pve-acme-directory-url', {
> +		default => $acme_default_directory_url,
> +		optional => 1,
> +	    }),
> +	},
> +    },
> +    returns => {
> +	type => 'string',
> +    },
> +    code => sub {
> +	my ($param) = @_;
> +
> +	my $account_name = extract_param($param, 'name') // 'default';
> +	my $account_file = "${acme_account_dir}/${account_name}";
> +
> +	mkdir $acme_account_dir;
> +
> +	raise_param_exc({'name' => "ACME account config file '${account_name}' already exists."})
> +	    if -e $account_file;
> +
> +	my $directory = extract_param($param, 'directory') // $acme_default_directory_url;
> +	my $contact = $account_contact_from_param->($param);
> +
> +	my $rpcenv = PVE::RPCEnvironment::get();
> +
> +	my $authuser = $rpcenv->get_user();
> +
> +	my $realcmd = sub {
> +	    PVE::Cluster::cfs_lock_acme($account_name, 10, sub {
> +		die "ACME account config file '${account_name}' already exists.\n"
> +		    if -e $account_file;
> +
> +		my $acme = PVE::ACME->new($account_file, $directory);
> +		print "Generating ACME account key..\n";
> +		$acme->init(4096);
> +		print "Registering ACME account..\n";
> +		eval { $acme->new_account($param->{tos_url}, contact => $contact); };
> +		if ($@) {
> +		    warn "$@\n";
> +		    unlink $account_file;
> +		    die "Registration failed!\n";
> +		}
> +		print "Registration successful, account URL: '$acme->{location}'\n";
> +	    });
> +	    die $@ if $@;
> +	};
> +
> +	return $rpcenv->fork_worker('acmeregister', undef, $authuser, $realcmd);
> +    }});
> +
> +my $update_account = sub {
> +    my ($param, $msg, %info) = @_;
> +
> +    my $account_name = extract_param($param, 'name');
> +    my $account_file = "${acme_account_dir}/${account_name}";
> +
> +    raise_param_exc({'name' => "ACME account config file '${account_name}' does not exist."})
> +	if ! -e $account_file;
> +
> +
> +    my $rpcenv = PVE::RPCEnvironment::get();
> +
> +    my $authuser = $rpcenv->get_user();
> +
> +    my $realcmd = sub {
> +	PVE::Cluster::cfs_lock_acme($account_name, 10, sub {
> +	    die "ACME account config file '${account_name}' does not exist.\n"
> +		if ! -e $account_file;
> +
> +	    my $acme = PVE::ACME->new($account_file);
> +	    $acme->load();
> +	    $acme->update_account(%info);
> +	});
> +	die $@ if $@;
> +    };
> +
> +    return $rpcenv->fork_worker("acme${msg}", undef, $authuser, $realcmd);
> +};
> +
> +__PACKAGE__->register_method ({
> +    name => 'update_account',
> +    path => 'account/{name}',
> +    method => 'PUT',
> +    description => "Update existing ACME account information with CA. Note: not specifying any new account information triggers a refresh.",
> +    protected => 1,
> +    parameters => {
> +	additionalProperties => 0,
> +	properties => {
> +	    name => get_standard_option('pve-acme-account-name'),
> +	    contact => get_standard_option('pve-acme-account-contact', {
> +		optional => 1,
> +	    }),
> +	},
> +    },
> +    returns => {
> +	type => 'string',
> +    },
> +    code => sub {
> +	my ($param) = @_;
> +
> +	my $contact = $account_contact_from_param->($param);
> +	if (scalar @$contact) {
> +	    return $update_account->($param, 'update', contact => $contact);
> +	} else {
> +	    return $update_account->($param, 'refresh');
> +	}
> +    }});
> +
> +__PACKAGE__->register_method ({
> +    name => 'get_account',
> +    path => 'account/{name}',
> +    method => 'GET',
> +    description => "Return existing ACME account information.",
> +    protected => 1,
> +    parameters => {
> +	additionalProperties => 0,
> +	properties => {
> +	    name => get_standard_option('pve-acme-account-name'),
> +	},
> +    },
> +    returns => {
> +	type => 'object',
> +	additionalProperties => 0,
> +	properties => {
> +	    account => {
> +		type => 'object',
> +		optional => 1,
> +	    },
> +	    directory => get_standard_option('pve-acme-directory-url', {
> +		optional => 1,
> +	    }),
> +	    location => {
> +		type => 'string',
> +		optional => 1,
> +	    },
> +	    tos => {
> +		type => 'string',
> +		optional => 1,
> +	    },
> +	},
> +    },
> +    code => sub {
> +	my ($param) = @_;
> +
> +	my $account_name = extract_param($param, 'name');
> +	my $account_file = "${acme_account_dir}/${account_name}";
> +
> +	raise_param_exc({'name' => "ACME account config file '${account_name}' does not exist."})
> +	    if ! -e $account_file;
> +
> +	my $acme = PVE::ACME->new($account_file);
> +	$acme->load();
> +
> +	my $res = {};
> +	$res->{account} = $acme->{account};
> +	$res->{directory} = $acme->{directory};
> +	$res->{location} = $acme->{location};
> +	$res->{tos} = $acme->{tos};
> +
> +	return $res;
> +    }});
> +
> +__PACKAGE__->register_method ({
> +    name => 'deactivate_account',
> +    path => 'account/{name}',
> +    method => 'DELETE',
> +    description => "Deactivate existing ACME account at CA.",
> +    protected => 1,
> +    parameters => {
> +	additionalProperties => 0,
> +	properties => {
> +	    name => get_standard_option('pve-acme-account-name'),
> +	},
> +    },
> +    returns => {
> +	type => 'string',
> +    },
> +    code => sub {
> +	my ($param) = @_;
> +
> +	return $update_account->($param, 'deactivate', status => 'deactivated');
> +    }});
> +
> +__PACKAGE__->register_method ({
> +    name => 'get_tos',
> +    path => 'tos',
> +    method => 'GET',
> +    description => "Retrieve ACME TermsOfService URL from CA.",
> +    parameters => {
> +	additionalProperties => 0,
> +	properties => {
> +	    directory => get_standard_option('pve-acme-directory-url', {
> +		default => $acme_default_directory_url,
> +		optional => 1,
> +	    }),
> +	},
> +    },
> +    returns => {
> +	type => 'string',
> +	description => 'ACME TermsOfService URL.',
> +    },
> +    code => sub {
> +	my ($param) = @_;
> +
> +	my $directory = extract_param($param, 'directory') // $acme_default_directory_url;
> +
> +	my $acme = PVE::ACME->new(undef, $directory);
> +	my $meta = $acme->get_meta();
> +
> +	return $meta ? $meta->{termsOfService} : undef;
> +    }});
> +
> +__PACKAGE__->register_method ({
> +    name => 'get_directories',
> +    path => 'directories',
> +    method => 'GET',
> +    description => "Get named known ACME directory endpoints.",
> +    parameters => {
> +	additionalProperties => 0,
> +	properties => {},
> +    },
> +    returns => {
> +	type => 'array',
> +	items => {
> +	    type => 'object',
> +	    additionalProperties => 0,
> +	    properties => {
> +		name => {
> +		    type => 'string',
> +		},
> +		url => get_standard_option('pve-acme-directory-url'),
> +	    },
> +	},
> +    },
> +    code => sub {
> +	my ($param) = @_;
> +
> +	return $acme_directories;
> +    }});
> +
> +1;
> diff --git a/PVE/API2/Cluster.pm b/PVE/API2/Cluster.pm
> index 7f38e61c..2eac6b52 100644
> --- a/PVE/API2/Cluster.pm
> +++ b/PVE/API2/Cluster.pm
> @@ -24,6 +24,7 @@ use PVE::JSONSchema qw(get_standard_option);
>  use PVE::Firewall;
>  use PVE::API2::Firewall::Cluster;
>  use PVE::API2::ReplicationConfig;
> +use PVE::API2::ACMEAccount;
>  
>  use base qw(PVE::RESTHandler);
>  
> @@ -52,6 +53,11 @@ __PACKAGE__->register_method ({
>      path => 'ha',
>  });
>  
> +__PACKAGE__->register_method ({
> +    subclass => "PVE::API2::ACMEAccount",
> +    path => 'acme',
> +});
> +
>  my $dc_schema = PVE::Cluster::get_datacenter_schema();
>  my $dc_properties = { 
>      delete => {
> @@ -97,6 +103,7 @@ __PACKAGE__->register_method ({
>  	    { name => 'nextid' },
>  	    { name => 'firewall' },
>  	    { name => 'config' },
> +	    { name => 'acme' },
>  	    ];
>  
>  	return $result;
> 





More information about the pve-devel mailing list