]> git.proxmox.com Git - pve-manager.git/blob - PVE/API2/ACMEAccount.pm
bump version to 5.4-15
[pve-manager.git] / PVE / API2 / ACMEAccount.pm
1 package PVE::API2::ACMEAccount;
2
3 use strict;
4 use warnings;
5
6 use PVE::ACME;
7 use PVE::CertHelpers;
8 use PVE::Exception qw(raise_param_exc);
9 use PVE::JSONSchema qw(get_standard_option);
10 use PVE::RPCEnvironment;
11 use PVE::Tools qw(extract_param);
12
13 use base qw(PVE::RESTHandler);
14
15 my $acme_directories = [
16 {
17 name => 'Let\'s Encrypt V2',
18 url => 'https://acme-v02.api.letsencrypt.org/directory',
19 },
20 {
21 name => 'Let\'s Encrypt V2 Staging',
22 url => 'https://acme-staging-v02.api.letsencrypt.org/directory',
23 },
24 ];
25
26 my $acme_default_directory_url = $acme_directories->[0]->{url};
27
28 my $account_contact_from_param = sub {
29 my ($param) = @_;
30 return [ map { "mailto:$_" } PVE::Tools::split_list(extract_param($param, 'contact')) ];
31 };
32
33 my $acme_account_dir = PVE::CertHelpers::acme_account_dir();
34
35 __PACKAGE__->register_method ({
36 name => 'index',
37 path => '',
38 method => 'GET',
39 permissions => { user => 'all' },
40 description => "ACMEAccount index.",
41 parameters => {
42 additionalProperties => 0,
43 properties => {
44 },
45 },
46 returns => {
47 type => 'array',
48 items => {
49 type => "object",
50 properties => {},
51 },
52 links => [ { rel => 'child', href => "{name}" } ],
53 },
54 code => sub {
55 my ($param) = @_;
56
57 return [
58 { name => 'account' },
59 { name => 'tos' },
60 { name => 'directories' },
61 ];
62 }});
63
64 __PACKAGE__->register_method ({
65 name => 'account_index',
66 path => 'account',
67 method => 'GET',
68 permissions => { user => 'all' },
69 description => "ACMEAccount index.",
70 protected => 1,
71 parameters => {
72 additionalProperties => 0,
73 properties => {
74 },
75 },
76 returns => {
77 type => 'array',
78 items => {
79 type => "object",
80 properties => {},
81 },
82 links => [ { rel => 'child', href => "{name}" } ],
83 },
84 code => sub {
85 my ($param) = @_;
86
87 my $accounts = PVE::CertHelpers::list_acme_accounts();
88 return [ map { { name => $_ } } @$accounts ];
89 }});
90
91 __PACKAGE__->register_method ({
92 name => 'register_account',
93 path => 'account',
94 method => 'POST',
95 description => "Register a new ACME account with CA.",
96 protected => 1,
97 parameters => {
98 additionalProperties => 0,
99 properties => {
100 name => get_standard_option('pve-acme-account-name'),
101 contact => get_standard_option('pve-acme-account-contact'),
102 tos_url => {
103 type => 'string',
104 description => 'URL of CA TermsOfService - setting this indicates agreement.',
105 optional => 1,
106 },
107 directory => get_standard_option('pve-acme-directory-url', {
108 default => $acme_default_directory_url,
109 optional => 1,
110 }),
111 },
112 },
113 returns => {
114 type => 'string',
115 },
116 code => sub {
117 my ($param) = @_;
118
119 my $account_name = extract_param($param, 'name') // 'default';
120 my $account_file = "${acme_account_dir}/${account_name}";
121
122 mkdir $acme_account_dir;
123
124 raise_param_exc({'name' => "ACME account config file '${account_name}' already exists."})
125 if -e $account_file;
126
127 my $directory = extract_param($param, 'directory') // $acme_default_directory_url;
128 my $contact = $account_contact_from_param->($param);
129
130 my $rpcenv = PVE::RPCEnvironment::get();
131
132 my $authuser = $rpcenv->get_user();
133
134 my $realcmd = sub {
135 PVE::Cluster::cfs_lock_acme($account_name, 10, sub {
136 die "ACME account config file '${account_name}' already exists.\n"
137 if -e $account_file;
138
139 my $acme = PVE::ACME->new($account_file, $directory);
140 print "Generating ACME account key..\n";
141 $acme->init(4096);
142 print "Registering ACME account..\n";
143 eval { $acme->new_account($param->{tos_url}, contact => $contact); };
144 if ($@) {
145 warn "$@\n";
146 unlink $account_file;
147 die "Registration failed!\n";
148 }
149 print "Registration successful, account URL: '$acme->{location}'\n";
150 });
151 die $@ if $@;
152 };
153
154 return $rpcenv->fork_worker('acmeregister', undef, $authuser, $realcmd);
155 }});
156
157 my $update_account = sub {
158 my ($param, $msg, %info) = @_;
159
160 my $account_name = extract_param($param, 'name') // 'default';
161 my $account_file = "${acme_account_dir}/${account_name}";
162
163 raise_param_exc({'name' => "ACME account config file '${account_name}' does not exist."})
164 if ! -e $account_file;
165
166
167 my $rpcenv = PVE::RPCEnvironment::get();
168
169 my $authuser = $rpcenv->get_user();
170
171 my $realcmd = sub {
172 PVE::Cluster::cfs_lock_acme($account_name, 10, sub {
173 die "ACME account config file '${account_name}' does not exist.\n"
174 if ! -e $account_file;
175
176 my $acme = PVE::ACME->new($account_file);
177 $acme->load();
178 $acme->update_account(%info);
179 if ($info{status} && $info{status} eq 'deactivated') {
180 my $deactivated_name;
181 for my $i (0..100) {
182 my $candidate = "${acme_account_dir}/_deactivated_${account_name}_${i}";
183 if (! -e $candidate) {
184 $deactivated_name = $candidate;
185 last;
186 }
187 }
188 if ($deactivated_name) {
189 print "Renaming account file from '$account_file' to '$deactivated_name'\n";
190 rename($account_file, $deactivated_name) or
191 warn ".. failed - $!\n";
192 } else {
193 warn "No free slot to rename deactivated account file '$account_file', leaving in place\n";
194 }
195 }
196 });
197 die $@ if $@;
198 };
199
200 return $rpcenv->fork_worker("acme${msg}", undef, $authuser, $realcmd);
201 };
202
203 __PACKAGE__->register_method ({
204 name => 'update_account',
205 path => 'account/{name}',
206 method => 'PUT',
207 description => "Update existing ACME account information with CA. Note: not specifying any new account information triggers a refresh.",
208 protected => 1,
209 parameters => {
210 additionalProperties => 0,
211 properties => {
212 name => get_standard_option('pve-acme-account-name'),
213 contact => get_standard_option('pve-acme-account-contact', {
214 optional => 1,
215 }),
216 },
217 },
218 returns => {
219 type => 'string',
220 },
221 code => sub {
222 my ($param) = @_;
223
224 my $contact = $account_contact_from_param->($param);
225 if (scalar @$contact) {
226 return $update_account->($param, 'update', contact => $contact);
227 } else {
228 return $update_account->($param, 'refresh');
229 }
230 }});
231
232 __PACKAGE__->register_method ({
233 name => 'get_account',
234 path => 'account/{name}',
235 method => 'GET',
236 description => "Return existing ACME account information.",
237 protected => 1,
238 parameters => {
239 additionalProperties => 0,
240 properties => {
241 name => get_standard_option('pve-acme-account-name'),
242 },
243 },
244 returns => {
245 type => 'object',
246 additionalProperties => 0,
247 properties => {
248 account => {
249 type => 'object',
250 optional => 1,
251 renderer => 'yaml',
252 },
253 directory => get_standard_option('pve-acme-directory-url', {
254 optional => 1,
255 }),
256 location => {
257 type => 'string',
258 optional => 1,
259 },
260 tos => {
261 type => 'string',
262 optional => 1,
263 },
264 },
265 },
266 code => sub {
267 my ($param) = @_;
268
269 my $account_name = extract_param($param, 'name') // 'default';
270 my $account_file = "${acme_account_dir}/${account_name}";
271
272 raise_param_exc({'name' => "ACME account config file '${account_name}' does not exist."})
273 if ! -e $account_file;
274
275 my $acme = PVE::ACME->new($account_file);
276 $acme->load();
277
278 my $res = {};
279 $res->{account} = $acme->{account};
280 $res->{directory} = $acme->{directory};
281 $res->{location} = $acme->{location};
282 $res->{tos} = $acme->{tos};
283
284 return $res;
285 }});
286
287 __PACKAGE__->register_method ({
288 name => 'deactivate_account',
289 path => 'account/{name}',
290 method => 'DELETE',
291 description => "Deactivate existing ACME account at CA.",
292 protected => 1,
293 parameters => {
294 additionalProperties => 0,
295 properties => {
296 name => get_standard_option('pve-acme-account-name'),
297 },
298 },
299 returns => {
300 type => 'string',
301 },
302 code => sub {
303 my ($param) = @_;
304
305 return $update_account->($param, 'deactivate', status => 'deactivated');
306 }});
307
308 __PACKAGE__->register_method ({
309 name => 'get_tos',
310 path => 'tos',
311 method => 'GET',
312 description => "Retrieve ACME TermsOfService URL from CA.",
313 permissions => { user => 'all' },
314 parameters => {
315 additionalProperties => 0,
316 properties => {
317 directory => get_standard_option('pve-acme-directory-url', {
318 default => $acme_default_directory_url,
319 optional => 1,
320 }),
321 },
322 },
323 returns => {
324 type => 'string',
325 description => 'ACME TermsOfService URL.',
326 },
327 code => sub {
328 my ($param) = @_;
329
330 my $directory = extract_param($param, 'directory') // $acme_default_directory_url;
331
332 my $acme = PVE::ACME->new(undef, $directory);
333 my $meta = $acme->get_meta();
334
335 return $meta ? $meta->{termsOfService} : undef;
336 }});
337
338 __PACKAGE__->register_method ({
339 name => 'get_directories',
340 path => 'directories',
341 method => 'GET',
342 description => "Get named known ACME directory endpoints.",
343 permissions => { user => 'all' },
344 parameters => {
345 additionalProperties => 0,
346 properties => {},
347 },
348 returns => {
349 type => 'array',
350 items => {
351 type => 'object',
352 additionalProperties => 0,
353 properties => {
354 name => {
355 type => 'string',
356 },
357 url => get_standard_option('pve-acme-directory-url'),
358 },
359 },
360 },
361 code => sub {
362 my ($param) = @_;
363
364 return $acme_directories;
365 }});
366
367 1;