rules API: protect against concurrent updates
[pve-firewall.git] / src / PVE / API2 / Firewall / Rules.pm
1 package PVE::API2::Firewall::RulesBase;
2
3 use strict;
4 use warnings;
5 use PVE::JSONSchema qw(get_standard_option);
6 use PVE::Exception qw(raise raise_param_exc);
7
8 use PVE::Firewall;
9
10 use base qw(PVE::RESTHandler);
11
12 my $api_properties = { 
13     pos => {
14         description => "Rule position.",
15         type => 'integer',
16         minimum => 0,
17     },
18 };
19
20 sub load_config {
21     my ($class, $param) = @_;
22
23     die "implement this in subclass";
24
25     #return ($fw_conf, $rules);
26 }
27
28 sub save_rules {
29     my ($class, $param, $fw_conf, $rules) = @_;
30
31     die "implement this in subclass";
32 }
33
34 my $additional_param_hash = {};
35
36 sub allow_groups {
37     return 1;
38 }
39
40 sub additional_parameters {
41     my ($class, $new_value) = @_;
42
43     if (defined($new_value)) {
44         $additional_param_hash->{$class} = $new_value;
45     }
46
47     # return a copy
48     my $copy = {};
49     my $org = $additional_param_hash->{$class} || {};
50     foreach my $p (keys %$org) { $copy->{$p} = $org->{$p}; }
51     return $copy;
52 }
53
54 sub register_get_rules {
55     my ($class) = @_;
56
57     my $properties = $class->additional_parameters();
58
59     $class->register_method({
60         name => 'get_rules',
61         path => '',
62         method => 'GET',
63         description => "List rules.",
64         parameters => {
65             additionalProperties => 0,
66             properties => $properties,
67         },
68         returns => {
69             type => 'array',
70             items => {
71                 type => "object",
72                 properties => {
73                     pos => {
74                         type => 'integer',
75                     }
76                 },
77             },
78             links => [ { rel => 'child', href => "{pos}" } ],
79         },
80         code => sub {
81             my ($param) = @_;
82
83             my ($fw_conf, $rules) = $class->load_config($param);
84
85             my $digest = $fw_conf->{digest};
86
87             my $res = [];
88
89             my $ind = 0;
90             foreach my $rule (@$rules) {
91                 push @$res, PVE::Firewall::cleanup_fw_rule($rule, $digest, $ind++);
92             }
93
94             return $res;
95         }});
96 }
97
98 sub register_get_rule {
99     my ($class) = @_;
100
101     my $properties = $class->additional_parameters();
102
103     $properties->{pos} = $api_properties->{pos};
104     
105     $class->register_method({
106         name => 'get_rule',
107         path => '{pos}',
108         method => 'GET',
109         description => "Get single rule data.",
110         parameters => {
111             additionalProperties => 0,
112             properties => $properties,
113         },
114         returns => {
115             type => "object",
116             properties => {
117                 pos => {
118                     type => 'integer',
119                 }
120             },
121         },
122         code => sub {
123             my ($param) = @_;
124
125             my ($fw_conf, $rules) = $class->load_config($param);
126
127             my $digest = $fw_conf->{digest};
128         
129             die "no rule at position $param->{pos}\n" if $param->{pos} >= scalar(@$rules);
130         
131             my $rule = $rules->[$param->{pos}];
132             
133             return PVE::Firewall::cleanup_fw_rule($rule, $digest, $param->{pos});
134         }});
135 }
136
137 sub register_create_rule {
138     my ($class) = @_;
139
140     my $properties = $class->additional_parameters();
141
142     my $create_rule_properties = PVE::Firewall::add_rule_properties($properties);
143     $create_rule_properties->{action}->{optional} = 0;
144     $create_rule_properties->{type}->{optional} = 0;
145     
146     $class->register_method({
147         name => 'create_rule',
148         path => '',
149         method => 'POST',
150         description => "Create new rule.",
151         protected => 1,
152         parameters => {
153             additionalProperties => 0,
154             properties => $create_rule_properties,
155         },
156         returns => { type => "null" },
157         code => sub {
158             my ($param) = @_;
159
160             my ($fw_conf, $rules) = $class->load_config($param);
161
162             my $rule = {};
163
164             PVE::Firewall::copy_rule_data($rule, $param);
165             PVE::Firewall::verify_rule($rule, $class->allow_groups());
166
167             $rule->{enable} = 0 if !defined($param->{enable});
168
169             unshift @$rules, $rule;
170
171             $class->save_rules($param, $fw_conf, $rules);
172
173             return undef;
174         }});
175 }
176
177 sub register_update_rule {
178     my ($class) = @_;
179
180     my $properties = $class->additional_parameters();
181
182     $properties->{pos} = $api_properties->{pos};
183     
184     $properties->{moveto} = {
185         description => "Move rule to new position <moveto>. Other arguments are ignored.",
186         type => 'integer',
187         minimum => 0,
188         optional => 1,
189     };
190
191     $properties->{delete} = {
192         type => 'string', format => 'pve-configid-list',
193         description => "A list of settings you want to delete.",
194         optional => 1,
195     };
196
197     my $update_rule_properties = PVE::Firewall::add_rule_properties($properties);
198
199     $class->register_method({
200         name => 'update_rule',
201         path => '{pos}',
202         method => 'PUT',
203         description => "Modify rule data.",
204         protected => 1,
205         parameters => {
206             additionalProperties => 0,
207             properties => $update_rule_properties,
208         },
209         returns => { type => "null" },
210         code => sub {
211             my ($param) = @_;
212
213             my ($fw_conf, $rules) = $class->load_config($param);
214
215             PVE::Tools::assert_if_modified($fw_conf->{digest}, $param->{digest});
216
217             die "no rule at position $param->{pos}\n" if $param->{pos} >= scalar(@$rules);
218         
219             my $rule = $rules->[$param->{pos}];
220
221             my $moveto = $param->{moveto};
222             if (defined($moveto) && $moveto != $param->{pos}) {
223                 my $newrules = [];
224                 for (my $i = 0; $i < scalar(@$rules); $i++) {
225                     next if $i == $param->{pos};
226                     if ($i == $moveto) {
227                         push @$newrules, $rule;
228                     }
229                     push @$newrules, $rules->[$i];
230                 }
231                 push @$newrules, $rule if $moveto >= scalar(@$rules);
232                 $rules = $newrules;
233             } else {
234                 raise_param_exc({ type => "property is missing"})
235                     if !defined($param->{type});
236                 raise_param_exc({ action => "property is missing"})
237                     if !defined($param->{action});
238
239                 PVE::Firewall::copy_rule_data($rule, $param);
240                 
241                 PVE::Firewall::delete_rule_properties($rule, $param->{'delete'}) if $param->{'delete'};
242
243                 PVE::Firewall::verify_rule($rule, $class->allow_groups());
244             }
245
246             $class->save_rules($param, $fw_conf, $rules);
247
248             return undef;
249         }});
250 }
251
252 sub register_delete_rule {
253     my ($class) = @_;
254
255     my $properties = $class->additional_parameters();
256
257     $properties->{pos} = $api_properties->{pos};
258
259     $properties->{digest} = get_standard_option('pve-config-digest');
260     
261     $class->register_method({
262         name => 'delete_rule',
263         path => '{pos}',
264         method => 'DELETE',
265         description => "Delete rule.",
266         protected => 1,
267         parameters => {
268             additionalProperties => 0,
269             properties => $properties,
270         },
271         returns => { type => "null" },
272         code => sub {
273             my ($param) = @_;
274
275             my ($fw_conf, $rules) = $class->load_config($param);
276
277             PVE::Tools::assert_if_modified($fw_conf->{digest}, $param->{digest});
278         
279             die "no rule at position $param->{pos}\n" if $param->{pos} >= scalar(@$rules);
280         
281             splice(@$rules, $param->{pos}, 1);
282             
283             $class->save_rules($param, $fw_conf, $rules);
284
285             return undef;
286         }});
287 }
288
289 sub register_handlers {
290     my ($class) = @_;
291
292     $class->register_get_rules();
293     $class->register_get_rule();
294     $class->register_create_rule();
295     $class->register_update_rule();
296     $class->register_delete_rule();
297 }
298
299 package PVE::API2::Firewall::GroupRules;
300
301 use strict;
302 use warnings;
303 use PVE::JSONSchema qw(get_standard_option);
304
305 use base qw(PVE::API2::Firewall::RulesBase);
306
307 __PACKAGE__->additional_parameters({ group => get_standard_option('pve-security-group-name') });
308
309 sub allow_groups {
310     return 0;
311 }
312
313 sub load_config {
314     my ($class, $param) = @_;
315
316     my $fw_conf = PVE::Firewall::load_clusterfw_conf();
317     my $rules = $fw_conf->{groups}->{$param->{group}};
318     die "no such security group '$param->{group}'\n" if !defined($rules);
319
320     return ($fw_conf, $rules);
321 }
322
323 sub save_rules {
324     my ($class, $param, $fw_conf, $rules) = @_;
325
326     $fw_conf->{groups}->{$param->{group}} = $rules;
327     PVE::Firewall::save_clusterfw_conf($fw_conf);
328 }
329
330 __PACKAGE__->register_handlers();
331
332 package PVE::API2::Firewall::ClusterRules;
333
334 use strict;
335 use warnings;
336
337 use base qw(PVE::API2::Firewall::RulesBase);
338
339 sub load_config {
340     my ($class, $param) = @_;
341
342     my $fw_conf = PVE::Firewall::load_clusterfw_conf();
343     my $rules = $fw_conf->{rules};
344
345     return ($fw_conf, $rules);
346 }
347
348 sub save_rules {
349     my ($class, $param, $fw_conf, $rules) = @_;
350
351     $fw_conf->{rules} = $rules;
352     PVE::Firewall::save_clusterfw_conf($fw_conf);
353 }
354
355 __PACKAGE__->register_handlers();
356
357 package PVE::API2::Firewall::HostRules;
358
359 use strict;
360 use warnings;
361 use PVE::JSONSchema qw(get_standard_option);
362
363 use base qw(PVE::API2::Firewall::RulesBase);
364
365 __PACKAGE__->additional_parameters({ node => get_standard_option('pve-node')});
366
367 sub load_config {
368     my ($class, $param) = @_;
369
370     my $fw_conf = PVE::Firewall::load_hostfw_conf();
371     my $rules = $fw_conf->{rules};
372
373     return ($fw_conf, $rules);
374 }
375
376 sub save_rules {
377     my ($class, $param, $fw_conf, $rules) = @_;
378
379     $fw_conf->{rules} = $rules;
380     PVE::Firewall::save_hostfw_conf($fw_conf);
381 }
382
383 __PACKAGE__->register_handlers();
384
385 package PVE::API2::Firewall::VMRules;
386
387 use strict;
388 use warnings;
389 use PVE::JSONSchema qw(get_standard_option);
390
391 use base qw(PVE::API2::Firewall::RulesBase);
392
393 __PACKAGE__->additional_parameters({ 
394     node => get_standard_option('pve-node'),
395     vmid => get_standard_option('pve-vmid'),                               
396 });
397
398 sub load_config {
399     my ($class, $param) = @_;
400
401     my $fw_conf = PVE::Firewall::load_vmfw_conf($param->{vmid});
402     my $rules = $fw_conf->{rules};
403
404     return ($fw_conf, $rules);
405 }
406
407 sub save_rules {
408     my ($class, $param, $fw_conf, $rules) = @_;
409
410     $fw_conf->{rules} = $rules;
411     PVE::Firewall::save_vmfw_conf($param->{vmid}, $fw_conf);
412 }
413
414 __PACKAGE__->register_handlers();
415
416 1;