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