api: lock configs
[pve-firewall.git] / src / PVE / API2 / Firewall / IPSet.pm
1 package PVE::API2::Firewall::IPSetBase;
2
3 use strict;
4 use warnings;
5 use PVE::Exception qw(raise raise_param_exc);
6 use PVE::JSONSchema qw(get_standard_option);
7
8 use PVE::Firewall;
9
10 use base qw(PVE::RESTHandler);
11
12 my $api_properties = {
13     cidr => {
14         description => "Network/IP specification in CIDR format.",
15         type => 'string', format => 'IPorCIDRorAlias',
16     },
17     name => get_standard_option('ipset-name'),
18     comment => {
19         type => 'string',
20         optional => 1,
21     },
22     nomatch => {
23         type => 'boolean',
24         optional => 1,
25     },
26 };
27
28 sub lock_config {
29     my ($class, $param, $code) = @_;
30
31     die "implement this in subclass";
32 }
33
34 sub load_config {
35     my ($class, $param) = @_;
36
37     die "implement this in subclass";
38
39     #return ($cluster_conf, $fw_conf, $ipset);
40 }
41
42 sub save_config {
43     my ($class, $param, $fw_conf) = @_;
44
45     die "implement this in subclass";
46 }
47
48 sub rule_env {
49     my ($class, $param) = @_;
50
51     die "implement this in subclass";
52 }
53
54 sub save_ipset {
55     my ($class, $param, $fw_conf, $ipset) = @_;
56
57     if (!defined($ipset)) {
58         delete $fw_conf->{ipset}->{$param->{name}};
59     } else {
60         $fw_conf->{ipset}->{$param->{name}} = $ipset;
61     }
62
63     $class->save_config($param, $fw_conf);
64 }
65
66 my $additional_param_hash = {};
67
68 sub additional_parameters {
69     my ($class, $new_value) = @_;
70
71     if (defined($new_value)) {
72         $additional_param_hash->{$class} = $new_value;
73     }
74
75     # return a copy
76     my $copy = {};
77     my $org = $additional_param_hash->{$class} || {};
78     foreach my $p (keys %$org) { $copy->{$p} = $org->{$p}; }
79     return $copy;
80 }
81
82 sub register_get_ipset {
83     my ($class) = @_;
84
85     my $properties = $class->additional_parameters();
86
87     $properties->{name} = $api_properties->{name};
88
89     $class->register_method({
90         name => 'get_ipset',
91         path => '',
92         method => 'GET',
93         description => "List IPSet content",
94         permissions => PVE::Firewall::rules_audit_permissions($class->rule_env()),
95         parameters => {
96             additionalProperties => 0,
97             properties => $properties,
98         },
99         returns => {
100             type => 'array',
101             items => {
102                 type => "object",
103                 properties => {
104                     cidr => {
105                         type => 'string',
106                     },
107                     comment => {
108                         type => 'string',
109                         optional => 1,
110                     },
111                     nomatch => {
112                         type => 'boolean',
113                         optional => 1,
114                     },
115                     digest => get_standard_option('pve-config-digest', { optional => 0} ),
116                 },
117             },
118             links => [ { rel => 'child', href => "{cidr}" } ],
119         },
120         code => sub {
121             my ($param) = @_;
122
123             my ($cluster_conf, $fw_conf, $ipset) = $class->load_config($param);
124
125             return PVE::Firewall::copy_list_with_digest($ipset);
126         }});
127 }
128
129 sub register_delete_ipset {
130     my ($class) = @_;
131
132     my $properties = $class->additional_parameters();
133
134     $properties->{name} = get_standard_option('ipset-name');
135
136     $class->register_method({
137         name => 'delete_ipset',
138         path => '',
139         method => 'DELETE',
140         description => "Delete IPSet",
141         protected => 1,
142         permissions => PVE::Firewall::rules_modify_permissions($class->rule_env()),
143         parameters => {
144             additionalProperties => 0,
145             properties => $properties,
146         },
147         returns => { type => 'null' },
148         code => sub {
149             my ($param) = @_;
150
151             $class->lock_config($param, sub {
152                 my ($param) = @_;
153
154                 my ($cluster_conf, $fw_conf, $ipset) = $class->load_config($param);
155
156                 die "IPSet '$param->{name}' is not empty\n"
157                     if scalar(@$ipset);
158
159                 $class->save_ipset($param, $fw_conf, undef);
160
161             });
162
163             return undef;
164         }});
165 }
166
167 sub register_create_ip {
168     my ($class) = @_;
169
170     my $properties = $class->additional_parameters();
171
172     $properties->{name} = $api_properties->{name};
173     $properties->{cidr} = $api_properties->{cidr};
174     $properties->{nomatch} = $api_properties->{nomatch};
175     $properties->{comment} = $api_properties->{comment};
176
177     $class->register_method({
178         name => 'create_ip',
179         path => '',
180         method => 'POST',
181         description => "Add IP or Network to IPSet.",
182         protected => 1,
183         permissions => PVE::Firewall::rules_modify_permissions($class->rule_env()),
184         parameters => {
185             additionalProperties => 0,
186             properties => $properties,
187         },
188         returns => { type => "null" },
189         code => sub {
190             my ($param) = @_;
191
192             $class->lock_config($param, sub {
193                 my ($param) = @_;
194
195                 my ($cluster_conf, $fw_conf, $ipset) = $class->load_config($param);
196
197                 my $cidr = $param->{cidr};
198
199                 foreach my $entry (@$ipset) {
200                     raise_param_exc({ cidr => "address '$cidr' already exists" })
201                         if $entry->{cidr} eq $cidr;
202                 }
203
204                 raise_param_exc({ cidr => "a zero prefix is not allowed in ipset entries" })
205                     if $cidr =~ m!/0+$!;
206
207                 # make sure alias exists (if $cidr is an alias)
208                 PVE::Firewall::resolve_alias($cluster_conf, $fw_conf, $cidr)
209                     if $cidr =~ m/^${PVE::Firewall::ip_alias_pattern}$/;
210
211                 my $data = { cidr => $cidr };
212
213                 $data->{nomatch} = 1 if $param->{nomatch};
214                 $data->{comment} = $param->{comment} if $param->{comment};
215
216                 unshift @$ipset, $data;
217
218                 $class->save_ipset($param, $fw_conf, $ipset);
219
220             });
221
222             return undef;
223         }});
224 }
225
226 sub register_read_ip {
227     my ($class) = @_;
228
229     my $properties = $class->additional_parameters();
230
231     $properties->{name} = $api_properties->{name};
232     $properties->{cidr} = $api_properties->{cidr};
233
234     $class->register_method({
235         name => 'read_ip',
236         path => '{cidr}',
237         method => 'GET',
238         description => "Read IP or Network settings from IPSet.",
239         permissions => PVE::Firewall::rules_audit_permissions($class->rule_env()),
240         protected => 1,
241         parameters => {
242             additionalProperties => 0,
243             properties => $properties,
244         },
245         returns => { type => "object" },
246         code => sub {
247             my ($param) = @_;
248
249             my ($cluster_conf, $fw_conf, $ipset) = $class->load_config($param);
250
251             my $list = PVE::Firewall::copy_list_with_digest($ipset);
252
253             foreach my $entry (@$list) {
254                 if ($entry->{cidr} eq $param->{cidr}) {
255                     return $entry;
256                 }
257             }
258
259             raise_param_exc({ cidr => "no such IP/Network" });
260         }});
261 }
262
263 sub register_update_ip {
264     my ($class) = @_;
265
266     my $properties = $class->additional_parameters();
267
268     $properties->{name} = $api_properties->{name};
269     $properties->{cidr} = $api_properties->{cidr};
270     $properties->{nomatch} = $api_properties->{nomatch};
271     $properties->{comment} = $api_properties->{comment};
272     $properties->{digest} = get_standard_option('pve-config-digest');
273
274     $class->register_method({
275         name => 'update_ip',
276         path => '{cidr}',
277         method => 'PUT',
278         description => "Update IP or Network settings",
279         protected => 1,
280         permissions => PVE::Firewall::rules_modify_permissions($class->rule_env()),
281         parameters => {
282             additionalProperties => 0,
283             properties => $properties,
284         },
285         returns => { type => "null" },
286         code => sub {
287             my ($param) = @_;
288
289             my $found = $class->lock_config($param, sub {
290                 my ($param) = @_;
291
292                 my ($cluster_conf, $fw_conf, $ipset) = $class->load_config($param);
293
294                 my (undef, $digest) = PVE::Firewall::copy_list_with_digest($ipset);
295                 PVE::Tools::assert_if_modified($digest, $param->{digest});
296
297                 foreach my $entry (@$ipset) {
298                     if($entry->{cidr} eq $param->{cidr}) {
299                         $entry->{nomatch} = $param->{nomatch};
300                         $entry->{comment} = $param->{comment};
301                         $class->save_ipset($param, $fw_conf, $ipset);
302                         return 1;
303                     }
304                 }
305
306                 return 0;
307             });
308
309             return if $found;
310
311             raise_param_exc({ cidr => "no such IP/Network" });
312         }});
313 }
314
315 sub register_delete_ip {
316     my ($class) = @_;
317
318     my $properties = $class->additional_parameters();
319
320     $properties->{name} = $api_properties->{name};
321     $properties->{cidr} = $api_properties->{cidr};
322     $properties->{digest} = get_standard_option('pve-config-digest');
323
324     $class->register_method({
325         name => 'remove_ip',
326         path => '{cidr}',
327         method => 'DELETE',
328         description => "Remove IP or Network from IPSet.",
329         protected => 1,
330         permissions => PVE::Firewall::rules_modify_permissions($class->rule_env()),
331         parameters => {
332             additionalProperties => 0,
333             properties => $properties,
334         },
335         returns => { type => "null" },
336         code => sub {
337             my ($param) = @_;
338
339             $class->lock_config($param, sub {
340                 my ($param) = @_;
341
342                 my ($cluster_conf, $fw_conf, $ipset) = $class->load_config($param);
343
344                 my (undef, $digest) = PVE::Firewall::copy_list_with_digest($ipset);
345                 PVE::Tools::assert_if_modified($digest, $param->{digest});
346
347                 my $new = [];
348
349                 foreach my $entry (@$ipset) {
350                     push @$new, $entry if $entry->{cidr} ne $param->{cidr};
351                 }
352
353                 $class->save_ipset($param, $fw_conf, $new);
354             });
355
356             return undef;
357         }});
358 }
359
360 sub register_handlers {
361     my ($class) = @_;
362
363     $class->register_delete_ipset();
364     $class->register_get_ipset();
365     $class->register_create_ip();
366     $class->register_read_ip();
367     $class->register_update_ip();
368     $class->register_delete_ip();
369 }
370
371 package PVE::API2::Firewall::ClusterIPset;
372
373 use strict;
374 use warnings;
375
376 use base qw(PVE::API2::Firewall::IPSetBase);
377
378 sub rule_env {
379     my ($class, $param) = @_;
380
381     return 'cluster';
382 }
383
384 sub lock_config {
385     my ($class, $param, $code) = @_;
386
387     PVE::Firewall::lock_clusterfw_conf(10, $code, $param);
388 }
389
390 sub load_config {
391     my ($class, $param) = @_;
392
393     my $fw_conf = PVE::Firewall::load_clusterfw_conf();
394     my $ipset = $fw_conf->{ipset}->{$param->{name}};
395     die "no such IPSet '$param->{name}'\n" if !defined($ipset);
396
397     return (undef, $fw_conf, $ipset);
398 }
399
400 sub save_config {
401     my ($class, $param, $fw_conf) = @_;
402
403     PVE::Firewall::save_clusterfw_conf($fw_conf);
404 }
405
406 __PACKAGE__->register_handlers();
407
408 package PVE::API2::Firewall::VMIPset;
409
410 use strict;
411 use warnings;
412 use PVE::JSONSchema qw(get_standard_option);
413
414 use base qw(PVE::API2::Firewall::IPSetBase);
415
416 sub rule_env {
417     my ($class, $param) = @_;
418
419     return 'vm';
420 }
421
422 __PACKAGE__->additional_parameters({
423     node => get_standard_option('pve-node'),
424     vmid => get_standard_option('pve-vmid'),
425 });
426
427 sub lock_config {
428     my ($class, $param, $code) = @_;
429
430     PVE::Firewall::lock_vmfw_conf($param->{vmid}, 10, $code, $param);
431 }
432
433 sub load_config {
434     my ($class, $param) = @_;
435
436     my $cluster_conf = PVE::Firewall::load_clusterfw_conf();
437     my $fw_conf = PVE::Firewall::load_vmfw_conf($cluster_conf, 'vm', $param->{vmid});
438     my $ipset = $fw_conf->{ipset}->{$param->{name}};
439     die "no such IPSet '$param->{name}'\n" if !defined($ipset);
440
441     return ($cluster_conf, $fw_conf, $ipset);
442 }
443
444 sub save_config {
445     my ($class, $param, $fw_conf) = @_;
446
447     PVE::Firewall::save_vmfw_conf($param->{vmid}, $fw_conf);
448 }
449
450 __PACKAGE__->register_handlers();
451
452 package PVE::API2::Firewall::CTIPset;
453
454 use strict;
455 use warnings;
456 use PVE::JSONSchema qw(get_standard_option);
457
458 use base qw(PVE::API2::Firewall::IPSetBase);
459
460 sub rule_env {
461     my ($class, $param) = @_;
462
463     return 'ct';
464 }
465
466 __PACKAGE__->additional_parameters({
467     node => get_standard_option('pve-node'),
468     vmid => get_standard_option('pve-vmid'),
469 });
470
471 sub lock_config {
472     my ($class, $param, $code) = @_;
473
474     PVE::Firewall::lock_vmfw_conf($param->{vmid}, 10, $code, $param);
475 }
476
477 sub load_config {
478     my ($class, $param) = @_;
479
480     my $cluster_conf = PVE::Firewall::load_clusterfw_conf();
481     my $fw_conf = PVE::Firewall::load_vmfw_conf($cluster_conf, 'ct', $param->{vmid});
482     my $ipset = $fw_conf->{ipset}->{$param->{name}};
483     die "no such IPSet '$param->{name}'\n" if !defined($ipset);
484
485     return ($cluster_conf, $fw_conf, $ipset);
486 }
487
488 sub save_config {
489     my ($class, $param, $fw_conf) = @_;
490
491     PVE::Firewall::save_vmfw_conf($param->{vmid}, $fw_conf);
492 }
493
494 __PACKAGE__->register_handlers();
495
496 package PVE::API2::Firewall::BaseIPSetList;
497
498 use strict;
499 use warnings;
500 use PVE::JSONSchema qw(get_standard_option);
501 use PVE::Exception qw(raise_param_exc);
502 use PVE::Firewall;
503
504 use base qw(PVE::RESTHandler);
505
506 sub lock_config {
507     my ($class, $param, $code) = @_;
508
509     die "implement this in subclass";
510 }
511
512 sub load_config {
513     my ($class, $param) = @_;
514
515     die "implement this in subclass";
516
517     #return ($cluster_conf, $fw_conf);
518 }
519
520 sub save_config {
521     my ($class, $param, $fw_conf) = @_;
522
523     die "implement this in subclass";
524 }
525
526 sub rule_env {
527     my ($class, $param) = @_;
528
529     die "implement this in subclass";
530 }
531
532 my $additional_param_hash_list = {};
533
534 sub additional_parameters {
535     my ($class, $new_value) = @_;
536
537     if (defined($new_value)) {
538         $additional_param_hash_list->{$class} = $new_value;
539     }
540
541     # return a copy
542     my $copy = {};
543     my $org = $additional_param_hash_list->{$class} || {};
544     foreach my $p (keys %$org) { $copy->{$p} = $org->{$p}; }
545     return $copy;
546 }
547
548 my $get_ipset_list = sub {
549     my ($fw_conf) = @_;
550
551     my $res = [];
552     foreach my $name (sort keys %{$fw_conf->{ipset}}) {
553         my $data = {
554             name => $name,
555         };
556         if (my $comment = $fw_conf->{ipset_comments}->{$name}) {
557             $data->{comment} = $comment;
558         }
559         push @$res, $data;
560     }
561
562     my ($list, $digest) = PVE::Firewall::copy_list_with_digest($res);
563
564     return wantarray ? ($list, $digest) : $list;
565 };
566
567 sub register_index {
568     my ($class) = @_;
569
570     my $properties = $class->additional_parameters();
571
572     $class->register_method({
573         name => 'ipset_index',
574         path => '',
575         method => 'GET',
576         description => "List IPSets",
577         permissions => PVE::Firewall::rules_audit_permissions($class->rule_env()),
578         parameters => {
579             additionalProperties => 0,
580             properties => $properties,
581         },
582         returns => {
583             type => 'array',
584             items => {
585                 type => "object",
586                 properties => {
587                     name => get_standard_option('ipset-name'),
588                     digest => get_standard_option('pve-config-digest', { optional => 0} ),
589                     comment => {
590                         type => 'string',
591                         optional => 1,
592                     }
593                 },
594             },
595             links => [ { rel => 'child', href => "{name}" } ],
596         },
597         code => sub {
598             my ($param) = @_;
599
600             my ($cluster_conf, $fw_conf) = $class->load_config($param);
601
602             return &$get_ipset_list($fw_conf);
603         }});
604 }
605
606 sub register_create {
607     my ($class) = @_;
608
609     my $properties = $class->additional_parameters();
610
611     $properties->{name} = get_standard_option('ipset-name');
612
613     $properties->{comment} = { type => 'string', optional => 1 };
614
615     $properties->{digest} = get_standard_option('pve-config-digest');
616
617     $properties->{rename} = get_standard_option('ipset-name', {
618         description => "Rename an existing IPSet. You can set 'rename' to the same value as 'name' to update the 'comment' of an existing IPSet.",
619         optional => 1 });
620
621     $class->register_method({
622         name => 'create_ipset',
623         path => '',
624         method => 'POST',
625         description => "Create new IPSet",
626         protected => 1,
627         permissions => PVE::Firewall::rules_modify_permissions($class->rule_env()),
628         parameters => {
629             additionalProperties => 0,
630             properties => $properties,
631         },
632         returns => { type => 'null' },
633         code => sub {
634             my ($param) = @_;
635
636             $class->lock_config($param, sub {
637                 my ($param) = @_;
638
639                 my ($cluster_conf, $fw_conf) = $class->load_config($param);
640
641                 if ($param->{rename}) {
642                     my (undef, $digest) = &$get_ipset_list($fw_conf);
643                     PVE::Tools::assert_if_modified($digest, $param->{digest});
644
645                     raise_param_exc({ name => "IPSet '$param->{rename}' does not exist" })
646                         if !$fw_conf->{ipset}->{$param->{rename}};
647
648                     # prevent overwriting existing ipset
649                     raise_param_exc({ name => "IPSet '$param->{name}' does already exist"})
650                         if $fw_conf->{ipset}->{$param->{name}} &&
651                         $param->{name} ne $param->{rename};
652
653                     my $data = delete $fw_conf->{ipset}->{$param->{rename}};
654                     $fw_conf->{ipset}->{$param->{name}} = $data;
655                     if (my $comment = delete $fw_conf->{ipset_comments}->{$param->{rename}}) {
656                         $fw_conf->{ipset_comments}->{$param->{name}} = $comment;
657                     }
658                     $fw_conf->{ipset_comments}->{$param->{name}} = $param->{comment} if defined($param->{comment});
659                 } else {
660                     foreach my $name (keys %{$fw_conf->{ipset}}) {
661                         raise_param_exc({ name => "IPSet '$name' already exists" })
662                             if $name eq $param->{name};
663                     }
664
665                     $fw_conf->{ipset}->{$param->{name}} = [];
666                     $fw_conf->{ipset_comments}->{$param->{name}} = $param->{comment} if defined($param->{comment});
667                 }
668
669                 $class->save_config($param, $fw_conf);
670             });
671
672             return undef;
673         }});
674 }
675
676 sub register_handlers {
677     my ($class) = @_;
678
679     $class->register_index();
680     $class->register_create();
681 }
682
683 package PVE::API2::Firewall::ClusterIPSetList;
684
685 use strict;
686 use warnings;
687 use PVE::Firewall;
688
689 use base qw(PVE::API2::Firewall::BaseIPSetList);
690
691 sub rule_env {
692     my ($class, $param) = @_;
693
694     return 'cluster';
695 }
696
697 sub lock_config {
698     my ($class, $param, $code) = @_;
699
700     PVE::Firewall::lock_clusterfw_conf(10, $code, $param);
701 }
702
703 sub load_config {
704     my ($class, $param) = @_;
705
706     my $cluster_conf = PVE::Firewall::load_clusterfw_conf();
707     return (undef, $cluster_conf);
708 }
709
710 sub save_config {
711     my ($class, $param, $fw_conf) = @_;
712
713     PVE::Firewall::save_clusterfw_conf($fw_conf);
714 }
715
716 __PACKAGE__->register_handlers();
717
718 __PACKAGE__->register_method ({
719     subclass => "PVE::API2::Firewall::ClusterIPset",
720     path => '{name}',
721     # set fragment delimiter (no subdirs) - we need that, because CIDR address contain a slash '/'
722     fragmentDelimiter => '',
723 });
724
725 package PVE::API2::Firewall::VMIPSetList;
726
727 use strict;
728 use warnings;
729 use PVE::JSONSchema qw(get_standard_option);
730 use PVE::Firewall;
731
732 use base qw(PVE::API2::Firewall::BaseIPSetList);
733
734 __PACKAGE__->additional_parameters({
735     node => get_standard_option('pve-node'),
736     vmid => get_standard_option('pve-vmid'),
737 });
738
739 sub rule_env {
740     my ($class, $param) = @_;
741
742     return 'vm';
743 }
744
745 sub lock_config {
746     my ($class, $param, $code) = @_;
747
748     PVE::Firewall::lock_vmfw_conf($param->{vmid}, 10, $code, $param);
749 }
750
751 sub load_config {
752     my ($class, $param) = @_;
753
754     my $cluster_conf = PVE::Firewall::load_clusterfw_conf();
755     my $fw_conf = PVE::Firewall::load_vmfw_conf($cluster_conf, 'vm', $param->{vmid});
756     return ($cluster_conf, $fw_conf);
757 }
758
759 sub save_config {
760     my ($class, $param, $fw_conf) = @_;
761
762     PVE::Firewall::save_vmfw_conf($param->{vmid}, $fw_conf);
763 }
764
765 __PACKAGE__->register_handlers();
766
767 __PACKAGE__->register_method ({
768     subclass => "PVE::API2::Firewall::VMIPset",
769     path => '{name}',
770     # set fragment delimiter (no subdirs) - we need that, because CIDR address contain a slash '/'
771     fragmentDelimiter => '',
772 });
773
774 package PVE::API2::Firewall::CTIPSetList;
775
776 use strict;
777 use warnings;
778 use PVE::JSONSchema qw(get_standard_option);
779 use PVE::Firewall;
780
781 use base qw(PVE::API2::Firewall::BaseIPSetList);
782
783 __PACKAGE__->additional_parameters({
784     node => get_standard_option('pve-node'),
785     vmid => get_standard_option('pve-vmid'),
786 });
787
788 sub rule_env {
789     my ($class, $param) = @_;
790
791     return 'ct';
792 }
793
794 sub lock_config {
795     my ($class, $param, $code) = @_;
796
797     PVE::Firewall::lock_vmfw_conf($param->{vmid}, 10, $code, $param);
798 }
799
800 sub load_config {
801     my ($class, $param) = @_;
802
803     my $cluster_conf = PVE::Firewall::load_clusterfw_conf();
804     my $fw_conf = PVE::Firewall::load_vmfw_conf($cluster_conf, 'ct', $param->{vmid});
805     return ($cluster_conf, $fw_conf);
806 }
807
808 sub save_config {
809     my ($class, $param, $fw_conf) = @_;
810
811     PVE::Firewall::save_vmfw_conf($param->{vmid}, $fw_conf);
812 }
813
814 __PACKAGE__->register_handlers();
815
816 __PACKAGE__->register_method ({
817     subclass => "PVE::API2::Firewall::CTIPset",
818     path => '{name}',
819     # set fragment delimiter (no subdirs) - we need that, because CIDR address contain a slash '/'
820     fragmentDelimiter => '',
821 });
822
823 1;