]> git.proxmox.com Git - pmg-api.git/blob - src/PMG/CLI/pmgconfig.pm
dkim: add QID in warnings
[pmg-api.git] / src / PMG / CLI / pmgconfig.pm
1 package PMG::CLI::pmgconfig;
2
3 use strict;
4 use warnings;
5 use IO::File;
6 use Data::Dumper;
7
8 use Term::ReadLine;
9
10 use PVE::SafeSyslog;
11 use PVE::Tools qw(extract_param);
12 use PVE::INotify;
13 use PVE::CLIHandler;
14 use PVE::JSONSchema qw(get_standard_option);
15
16 use PMG::RESTEnvironment;
17 use PMG::RuleDB;
18 use PMG::RuleCache;
19 use PMG::Cluster;
20 use PMG::LDAPConfig;
21 use PMG::LDAPSet;
22 use PMG::Config;
23 use PMG::Ticket;
24
25 use PMG::API2::ACME;
26 use PMG::API2::ACMEPlugin;
27 use PMG::API2::Certificates;
28 use PMG::API2::DKIMSign;
29
30 use base qw(PVE::CLIHandler);
31
32 my $nodename = PVE::INotify::nodename();
33
34 sub setup_environment {
35 PMG::RESTEnvironment->setup_default_cli_env();
36 }
37
38 my $upid_exit = sub {
39 my $upid = shift;
40 my $status = PVE::Tools::upid_read_status($upid);
41 print "Task $status\n";
42 exit($status eq 'OK' ? 0 : -1);
43 };
44
45 sub param_mapping {
46 my ($name) = @_;
47
48 my $load_file_and_encode = sub {
49 my ($filename) = @_;
50
51 return PVE::ACME::Challenge->encode_value('string', 'data', PVE::Tools::file_get_contents($filename));
52 };
53
54 my $mapping = {
55 'upload_custom_cert' => [
56 'certificates',
57 'key',
58 ],
59 'add_plugin' => [
60 ['data', $load_file_and_encode, "File with one key-value pair per line, will be base64url encode for storage in plugin config.", 0],
61 ],
62 'update_plugin' => [
63 ['data', $load_file_and_encode, "File with one key-value pair per line, will be base64url encode for storage in plugin config.", 0],
64 ],
65 };
66
67 return $mapping->{$name};
68 }
69
70 __PACKAGE__->register_method ({
71 name => 'dump',
72 path => 'dump',
73 method => 'POST',
74 description => "Print configuration setting which can be used in templates.",
75 parameters => {
76 additionalProperties => 0,
77 properties => {},
78 },
79 returns => { type => 'null'},
80 code => sub {
81 my ($param) = @_;
82
83 my $cfg = PMG::Config->new();
84 my $vars = $cfg->get_template_vars();
85
86 foreach my $realm (sort keys %$vars) {
87 foreach my $section (sort keys %{$vars->{$realm}}) {
88 my $secvalue = $vars->{$realm}->{$section} // '';
89 if (ref($secvalue)) {
90 foreach my $key (sort keys %{$vars->{$realm}->{$section}}) {
91 my $value = $vars->{$realm}->{$section}->{$key} // '';
92 print "$realm.$section.$key = $value\n";
93 }
94 } else {
95 print "$realm.$section = $secvalue\n";
96 }
97 }
98 }
99
100 return undef;
101 }});
102
103 __PACKAGE__->register_method ({
104 name => 'sync',
105 path => 'sync',
106 method => 'POST',
107 description => "Synchronize Proxmox Mail Gateway configurations with system configuration.",
108 parameters => {
109 additionalProperties => 0,
110 properties => {
111 restart => {
112 description => "Restart services if necessary.",
113 type => 'boolean',
114 default => 0,
115 optional => 1,
116 },
117 },
118 },
119 returns => { type => 'null'},
120 code => sub {
121 my ($param) = @_;
122
123 my $cfg = PMG::Config->new();
124
125 my $ruledb = PMG::RuleDB->new();
126 my $rulecache = PMG::RuleCache->new($ruledb);
127
128 $cfg->rewrite_config($rulecache, $param->{restart});
129
130 return undef;
131 }});
132
133 __PACKAGE__->register_method ({
134 name => 'ldapsync',
135 path => 'ldapsync',
136 method => 'POST',
137 description => "Synchronize the LDAP database.",
138 parameters => {
139 additionalProperties => 0,
140 properties => {},
141 },
142 returns => { type => 'null'},
143 code => sub {
144 my ($param) = @_;
145
146 my $ldap_cfg = PVE::INotify::read_file("pmg-ldap.conf");
147 PMG::LDAPSet::ldap_resync($ldap_cfg, 1);
148
149 return undef;
150 }});
151
152 __PACKAGE__->register_method ({
153 name => 'apicert',
154 path => 'apicert',
155 method => 'POST',
156 description => "Generate /etc/pmg/pmg-api.pem (self signed certificate for GUI and REST API).",
157 parameters => {
158 additionalProperties => 0,
159 properties => {
160 force => {
161 description => "Overwrite existing certificate.",
162 type => 'boolean',
163 optional => 1,
164 default => 0,
165 },
166 },
167 },
168 returns => { type => 'null'},
169 code => sub {
170 my ($param) = @_;
171
172 PMG::Ticket::generate_api_cert($param->{force});
173
174 return undef;
175 }});
176
177 __PACKAGE__->register_method ({
178 name => 'tlscert',
179 path => 'tlscert',
180 method => 'POST',
181 description => "Generate /etc/pmg/pmg-tls.pem (self signed certificate for encrypted SMTP traffic).",
182 parameters => {
183 additionalProperties => 0,
184 properties => {
185 force => {
186 description => "Overwrite existing certificate.",
187 type => 'boolean',
188 optional => 1,
189 default => 0,
190 },
191 },
192 },
193 returns => { type => 'null'},
194 code => sub {
195 my ($param) = @_;
196
197 PMG::Utils::gen_proxmox_tls_cert($param->{force});
198
199 return undef;
200 }});
201
202 __PACKAGE__->register_method ({
203 name => 'init',
204 path => 'init',
205 method => 'POST',
206 description => "Generate required files in /etc/pmg/",
207 parameters => {
208 additionalProperties => 0,
209 properties => {},
210 },
211 returns => { type => 'null'},
212 code => sub {
213 my ($param) = @_;
214
215 my $cfg = PMG::Config->new();
216
217 PMG::Ticket::generate_api_cert();
218 PMG::Ticket::generate_csrf_key();
219 PMG::Ticket::generate_auth_key();
220
221 if ($cfg->get('mail', 'tls')) {
222 PMG::Utils::gen_proxmox_tls_cert();
223 }
224
225 return undef;
226 }});
227
228 __PACKAGE__->register_method({
229 name => 'acme_register',
230 path => 'acme_register',
231 method => 'POST',
232 description => "Register a new ACME account with a compatible CA.",
233 parameters => {
234 additionalProperties => 0,
235 properties => {
236 name => get_standard_option('pmg-acme-account-name'),
237 contact => get_standard_option('pmg-acme-account-contact'),
238 directory => get_standard_option('pmg-acme-directory-url', {
239 optional => 1,
240 }),
241 },
242 },
243 returns => { type => 'null' },
244 code => sub {
245 my ($param) = @_;
246
247 my $custom_directory = 1;
248 if (!$param->{directory}) {
249 my $directories = PMG::API2::ACME->get_directories({});
250 print "Directory endpoints:\n";
251 my $i = 0;
252 while ($i < @$directories) {
253 print $i, ") ", $directories->[$i]->{name}, " (", $directories->[$i]->{url}, ")\n";
254 $i++;
255 }
256 print $i, ") Custom\n";
257
258 my $term = Term::ReadLine->new('pmgconfig');
259 my $get_dir_selection = sub {
260 my $selection = $term->readline("Enter selection: ");
261 if ($selection =~ /^(\d+)$/) {
262 $selection = $1;
263 if ($selection == $i) {
264 $param->{directory} = $term->readline("Enter custom URL: ");
265 return;
266 } elsif ($selection < $i && $selection >= 0) {
267 $param->{directory} = $directories->[$selection]->{url};
268 $custom_directory = 0;
269 return;
270 }
271 }
272 print "Invalid selection.\n";
273 };
274
275 my $attempts = 0;
276 while (!$param->{directory}) {
277 die "Aborting.\n" if $attempts > 3;
278 $get_dir_selection->();
279 $attempts++;
280 }
281 }
282
283 print "\nAttempting to fetch Terms of Service from '$param->{directory}'..\n";
284 my $meta = PMG::API2::ACME->get_meta({ directory => $param->{directory} });
285 if ($meta->{termsOfService}) {
286 my $tos = $meta->{termsOfService};
287 print "Terms of Service: $tos\n";
288 my $term = Term::ReadLine->new('pmgconfig');
289 my $agreed = $term->readline('Do you agree to the above terms? [y|N]: ');
290 die "Cannot continue without agreeing to ToS, aborting.\n"
291 if ($agreed !~ /^y$/i);
292
293 $param->{tos_url} = $tos;
294 } else {
295 print "No Terms of Service found, proceeding.\n";
296 }
297
298 my $eab_enabled = $meta->{externalAccountRequired};
299 if (!$eab_enabled && $custom_directory) {
300 my $term = Term::ReadLine->new('pmgconfig');
301 my $agreed = $term->readline('Do you want to use external account binding? [y|N]: ');
302 $eab_enabled = ($agreed =~ /^y$/i);
303 } elsif ($eab_enabled) {
304 print "The CA requires external account binding.\n";
305 }
306 if ($eab_enabled) {
307 print "You should have received a key id and a key from your CA.\n";
308 my $term = Term::ReadLine->new('pmgconfig');
309 my $eab_kid = $term->readline('Enter EAB key id: ');
310 my $eab_hmac_key = $term->readline('Enter EAB key: ');
311
312 $param->{'eab-kid'} = $eab_kid;
313 $param->{'eab-hmac-key'} = $eab_hmac_key;
314 }
315
316 print "\nAttempting to register account with '$param->{directory}'..\n";
317
318 $upid_exit->(PMG::API2::ACME->register_account($param));
319 }});
320
321 my $print_cert_info = sub {
322 my ($schema, $cert, $options) = @_;
323
324 my $order = [qw(filename fingerprint subject issuer notbefore notafter public-key-type public-key-bits san)];
325 PVE::CLIFormatter::print_api_result(
326 $cert, $schema, $order, { %$options, noheader => 1, sort_key => 0 });
327 };
328
329 our $cmddef = {
330 'dump' => [ __PACKAGE__, 'dump', []],
331 sync => [ __PACKAGE__, 'sync', []],
332 ldapsync => [ __PACKAGE__, 'ldapsync', []],
333 apicert => [ __PACKAGE__, 'apicert', []],
334 tlscert => [ __PACKAGE__, 'tlscert', []],
335 init => [ __PACKAGE__, 'init', []],
336 dkim_set => [ 'PMG::API2::DKIMSign', 'set_selector', []],
337 dkim_record => [ 'PMG::API2::DKIMSign', 'get_selector_info', [], undef,
338 sub {
339 my ($res) = @_;
340 die "no dkim_selector configured\n" if !defined($res->{record});
341 print "$res->{record}\n";
342 }],
343
344 cert => {
345 info => [ 'PMG::API2::Certificates', 'info', [], { node => $nodename }, sub {
346 my ($res, $schema, $options) = @_;
347
348 if (!$options->{'output-format'} || $options->{'output-format'} eq 'text') {
349 for my $cert (sort { $a->{filename} cmp $b->{filename} } @$res) {
350 $print_cert_info->($schema->{items}, $cert, $options);
351 }
352 } else {
353 PVE::CLIFormatter::print_api_result($res, $schema, undef, $options);
354 }
355
356 }, $PVE::RESTHandler::standard_output_options],
357 set => [ 'PMG::API2::Certificates', 'upload_custom_cert', ['type', 'certificates', 'key'], { node => $nodename }, sub {
358 my ($res, $schema, $options) = @_;
359 $print_cert_info->($schema, $res, $options);
360 }, $PVE::RESTHandler::standard_output_options],
361 delete => [ 'PMG::API2::Certificates', 'remove_custom_cert', ['type', 'restart'], { node => $nodename } ],
362 },
363
364 acme => {
365 account => {
366 list => [ 'PMG::API2::ACME', 'account_index', [], {}, sub {
367 my ($res) = @_;
368 for my $acc (@$res) {
369 print "$acc->{name}\n";
370 }
371 }],
372 register => [ __PACKAGE__, 'acme_register', ['name', 'contact'], {}, $upid_exit ],
373 deactivate => [ 'PMG::API2::ACME', 'deactivate_account', ['name'], {}, $upid_exit ],
374 info => [ 'PMG::API2::ACME', 'get_account', ['name'], {}, sub {
375 my ($data, $schema, $options) = @_;
376 PVE::CLIFormatter::print_api_result($data, $schema, undef, $options);
377 }, $PVE::RESTHandler::standard_output_options],
378 update => [ 'PMG::API2::ACME', 'update_account', ['name'], {}, $upid_exit ],
379 },
380 cert => {
381 order => [ 'PMG::API2::Certificates', 'new_acme_cert', ['type'], { node => $nodename }, $upid_exit ],
382
383
384 renew => [ 'PMG::API2::Certificates', 'renew_acme_cert', ['type'], { node => $nodename }, $upid_exit ],
385 revoke => [ 'PMG::API2::Certificates', 'revoke_acme_cert', ['type'], { node => $nodename }, $upid_exit ],
386 },
387 plugin => {
388 list => [ 'PMG::API2::ACMEPlugin', 'index', [], {}, sub {
389 my ($data, $schema, $options) = @_;
390 PVE::CLIFormatter::print_api_result($data, $schema, undef, $options);
391 }, $PVE::RESTHandler::standard_output_options ],
392 config => [ 'PMG::API2::ACMEPlugin', 'get_plugin_config', ['id'], {}, sub {
393 my ($data, $schema, $options) = @_;
394 PVE::CLIFormatter::print_api_result($data, $schema, undef, $options);
395 }, $PVE::RESTHandler::standard_output_options ],
396 add => [ 'PMG::API2::ACMEPlugin', 'add_plugin', ['type', 'id'] ],
397 set => [ 'PMG::API2::ACMEPlugin', 'update_plugin', ['id'] ],
398 remove => [ 'PMG::API2::ACMEPlugin', 'delete_plugin', ['id'] ],
399 },
400
401 },
402 };
403
404
405 1;