Skip to content

Commit c138176

Browse files
Updated contributor doc, migrated API endpoints from function based to class based for APIFirewallVirtualIPs and APIFirewallNAT, started migrating APIFirewallRules
1 parent 96ef7e4 commit c138176

22 files changed

Lines changed: 889 additions & 86 deletions

CONTRIBUTING.md

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,8 @@ not specified, the default values are assumed:
105105

106106
- `$this->privileges` : Allows you to set what pfSense permissions are required for clients to access this model. This
107107
utilizes the same permissions as the pfSense webConfigurator. Specify the privileges internal config value in array
108-
format. Defaults to `["page-all"]` which requires client to have the 'WebCfg - All Pages' permission.
108+
format. Defaults to `["page-all"]` which requires client to have the 'WebCfg - All Pages' permission. A list of pfSense
109+
privileges can be found here: https://github.com/pfsense/pfsense/blob/master/src/etc/inc/priv.defs.inc
109110

110111
- `$this->requires_auth` : Specify whether authentication and authorization is required for the API model. If set to
111112
`false` clients will not have to authenticate or have privilege to access. Defaults to `true`.
@@ -115,9 +116,6 @@ writing a model that tests user's local database credentials and do not want the
115116
auth mode you would specify `$this->set_auth_mode = "local";` to always force local authentication. Defaults to the
116117
API's configured auth mode in the /api/ webConfigurator page.
117118

118-
- `$this->validators` : Allows you to specify function calls that validate the API payload data. More information on
119-
writing model validators is included below. Defaults to `[]` which does not provide any validation.
120-
121119

122120
#### Other Base Model Properties ####
123121
There are other properties inherited from APIBaseModel that are not intended (and shouldn't be) overridden by your
@@ -128,11 +126,11 @@ custom API model:
128126
- `$this->validated_data` : An array for validators to use to populate data that has been validated
129127
- `$this->errors` : An array to populate any errors encountered. Should be an array of APIResponse values.
130128

131-
#### Writing API Model Validators ####
132-
By default, API models do not provide any sort of validation. You are responsible for writing the class methods to
133-
validate the request. Validator methods are essentially class methods that check specific field(s) in our
134-
`$this->initial_data` property and either adds them to our `$this->validated_data` property, or adds an error response
135-
to `$this->errors`.
129+
#### Overriding API Model Validation ####
130+
By default, API models do not provide any sort of validation. You are responsible for overriding the class method to
131+
validate the request. To validate requests, you must override the `validate_payload()` method to check specific field(s)
132+
in the `$this->initial_data` property and either add them to our `$this->validated_data` property if they are valid, or
133+
appends an error response to the `$this->errors` property.
136134

137135
For example:
138136
```php
@@ -145,15 +143,10 @@ class NewAPIModel extends APIBaseModel {
145143
$this->methods = ["POST"];
146144
$this->privileges = ["page-all", "page-diagnostics-arptable"];
147145
$this->requires_auth = false;
148-
149-
# CALL YOUR VALIDATOR METHODS HERE. This must be an array of function calls!
150-
$this->validators = [
151-
$this->validateTest()
152-
];
153146
}
154147

155-
# Create a new validator method that validates our payloads 'test' value.
156-
private function validateTest() {
148+
# Overrides our APIBaseModel validate_payload method to validate data within our initial_data property
149+
public function validate_payload() {
157150
# Check if we have a test value in our payload, if so add the value to validated_data array
158151
if (array_key_exists("test", $this->initial_data)) {
159152
$this->validated_data["test"] = $this->initial_data["test"];
@@ -186,11 +179,11 @@ class NewAPIModel extends APIBaseModel {
186179

187180
# CALL YOUR VALIDATOR METHODS HERE. This must be an array of function calls!
188181
$this->validators = [
189-
$this->validateTest()
182+
$this->validate_payload()
190183
];
191184
}
192185

193-
private function validateTest() {
186+
public function validate_payload() {
194187
if (array_key_exists("test", $this->initial_data)) {
195188
$this->validated_data["test"] = $this->initial_data["test"];
196189
}

pfSense-pkg-API/files/etc/inc/api.inc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -789,7 +789,7 @@ function sort_firewall_rules($mode=null, $data=null) {
789789
// Sorts nat rules by specified criteria and reloads the filter
790790
function sort_nat_rules($mode=null, $data=null) {
791791
// Variables
792-
global $err_lib, $config;
792+
global $config;
793793
$sort_arr = [];
794794
$master_arr = [];
795795
foreach ($config["nat"]["rule"] as $idx => $fre) {

pfSense-pkg-API/files/etc/inc/api/api_models/APIAccessToken.inc

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,15 @@ class APIAccessToken extends APIBaseModel {
88
parent::__construct();
99
$this->set_auth_mode = "local";
1010
$this->methods = ["POST"];
11-
$this->validators = [
12-
$this->validateAuthMode()
13-
];
1411
}
1512

1613
# Validate our API configurations auth mode (must be JWT)
17-
private function validateAuthMode() {
14+
public function validate_payload() {
1815
$api_config = APITools\get_api_config()[1];
1916

2017
# Add error if our auth mode is invalid
2118
if ($api_config["authmode"] !== "jwt") {
22-
return APIResponse\get(9);
19+
$this->errors[] = APIResponse\get(9);
2320
}
2421
}
2522

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
require_once("api/framework/APIBaseModel.inc");
3+
require_once("api/framework/APIResponse.inc");
4+
5+
class APIFirewallNatPortForwards extends APIBaseModel {
6+
# Create our method constructor
7+
public function __construct() {
8+
parent::__construct();
9+
$this->methods = ["GET"];
10+
$this->privileges = ["page-all", "page-firewall-nat-portforward"];
11+
}
12+
13+
public function action() {
14+
global $config;
15+
// Check that we have a virtual IP configuration
16+
if (!empty($config["nat"]["rule"])) {
17+
$this->validated_data = $config["nat"]["rule"];
18+
}
19+
return APIResponse\get(0, $this->validated_data);
20+
}
21+
}
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
<?php
2+
require_once("api/framework/APIBaseModel.inc");
3+
require_once("api/framework/APIResponse.inc");
4+
5+
class APIFirewallNatPortForwardsAdd extends APIBaseModel {
6+
# Create our method constructor
7+
public function __construct() {
8+
parent::__construct();
9+
$this->methods = ["POST"];
10+
$this->privileges = ["page-all", "page-firewall-nat-portforward-edit"];
11+
}
12+
13+
public function action() {
14+
global $config;
15+
$_SESSION["Username"] = $this->client->username;
16+
$change_note = " Added NAT rule via API"; // Add a change note
17+
$config["nat"]["rule"][] = $this->validated_data; // Write to our master config
18+
$next_rule_id = count($config["nat"]["rule"]); // Save our next rule ID
19+
APITools\sort_nat_rules($this->initial_data["top"], $next_rule_id); // Sort our nat rules
20+
write_config(sprintf(gettext($change_note))); // Apply our configuration change
21+
filter_configure(); // Ensure our firewall filter is reloaded
22+
return APIResponse\get(0, $this->validated_data);
23+
}
24+
25+
// TODO: break this down into smaller field specific validators
26+
public function validate_payload() {
27+
global $config;
28+
$user_created_msg = $_SESSION["Username"]."@".$_SERVER["REMOTE_ADDR"]." (API)"; // Save the username and ip of client
29+
$allowed_nat_ref = ["enable", "disable", "purenat"]; // Save our allow NAT reflection types
30+
$allowed_prot = ["tcp", "udp", "tcp/udp", "icmp", "esp", "ah", "gre", "ipv6", "igmp", "pim", "ospf"];
31+
if (isset($this->initial_data['interface'])) {
32+
$interface = $this->initial_data['interface'];
33+
$interface = APITools\get_pfsense_if_id($interface);
34+
} else {
35+
$this->errors[] = APIResponse\get(4000);
36+
}
37+
if (isset($this->initial_data['protocol'])) {
38+
$protocol = $this->initial_data['protocol'];
39+
} else {
40+
$this->errors[] = APIResponse\get(4001);
41+
42+
}
43+
if (isset($this->initial_data['target'])) {
44+
$localip = $this->initial_data['target'];
45+
} else {
46+
$this->errors[] = APIResponse\get(4002);
47+
}
48+
if (isset($this->initial_data['local-port'])) {
49+
$localport = $this->initial_data['local-port'];
50+
} else {
51+
$this->errors[] = APIResponse\get(4003);
52+
}
53+
if (isset($this->initial_data['src'])) {
54+
$src = $this->initial_data['src'];
55+
} else {
56+
$this->errors[] = APIResponse\get(4004);
57+
58+
}
59+
if (isset($this->initial_data['srcport'])) {
60+
$srcport = $this->initial_data['srcport'];
61+
}
62+
if (isset($this->initial_data['dst'])) {
63+
$dst = $this->initial_data['dst'];
64+
} else {
65+
$this->errors[] = APIResponse\get(4005);
66+
67+
}
68+
if (isset($this->initial_data['dstport'])) {
69+
$dstport = $this->initial_data['dstport'];
70+
}
71+
if (isset($this->initial_data['disabled'])) {
72+
if ($this->initial_data['disabled']) {
73+
$disabled = true;
74+
}
75+
}
76+
if (isset($this->initial_data['nordr'])) {
77+
if ($this->initial_data['nordr']) {
78+
$nordr = true;
79+
}
80+
}
81+
if (isset($this->initial_data['nosync'])) {
82+
if ($this->initial_data['nosync'] === true) {
83+
$nosync = true;
84+
}
85+
}
86+
if (isset($this->initial_data['top'])) {
87+
if ($this->initial_data['top']) {
88+
$this->initial_data = "top";
89+
}
90+
}
91+
if (isset($this->initial_data['descr'])) {
92+
$descr = $this->initial_data['descr'];
93+
}
94+
if (isset($this->initial_data['natreflection'])) {
95+
$natreflection = $this->initial_data['natreflection'];
96+
}
97+
98+
// INPUT VALIDATION/FORMATTING
99+
// Check that our required array/interface values are valid
100+
if (!is_string($interface)) {
101+
$this->errors[] = APIResponse\get(4006);
102+
} elseif (!in_array($protocol, $allowed_prot)) {
103+
$this->errors[] = APIResponse\get(4007);
104+
} elseif (isset($natreflection) and !in_array($natreflection, $allowed_nat_ref)) {
105+
$this->errors[] = APIResponse\get(4008);
106+
} elseif (!is_ipaddrv4($localip) and !alias_in_use($localip)) {
107+
$this->errors[] = APIResponse\get(4009);
108+
} elseif (!is_port_or_range($localport)) {
109+
$this->errors[] = APIResponse\get(4010);
110+
}
111+
$this->validated_data = array();
112+
// Check if rule is disabled
113+
if ($disabled) {
114+
$this->validated_data["disabled"] = "";
115+
}
116+
// Check if pfsync is disabled
117+
if ($nosync) {
118+
$this->validated_data["nosync"] = "";
119+
}
120+
// Check if RDR is disabled is disabled
121+
if ($nordr) {
122+
$this->validated_data["nordr"] = "";
123+
}
124+
// Check if user specified NAT reflection
125+
if ($natreflection) {
126+
$this->validated_data["natreflection"] = $natreflection;
127+
}
128+
$this->validated_data["interface"] = $interface;
129+
$this->validated_data["protocol"] = $protocol;
130+
$this->validated_data["source"] = array();
131+
$this->validated_data["destination"] = array();
132+
$this->validated_data["target"] = $localip;
133+
$this->validated_data["local-port"] = $localport;
134+
$this->validated_data["descr"] = $descr;
135+
$this->validated_data["associated-rule-id"] = "pass";
136+
$this->validated_data["created"] = array("time" => time(), "username" => $user_created_msg);
137+
$this->validated_data["updated"] = $this->validated_data["created"];
138+
// Check if our source and destination values are valid
139+
foreach (array("source" => $src, "destination" => $dst) as $dir => $val) {
140+
$dir_check = APITools\is_valid_rule_addr($val, $dir);
141+
if (!$dir_check["valid"]) {
142+
if ($dir === "source") {
143+
$this->errors[] = APIResponse\get(4011);
144+
} else {
145+
$this->errors[] = APIResponse\get(4012);
146+
147+
}
148+
} else {
149+
$this->validated_data = array_merge($this->validated_data, $dir_check["data"]);
150+
}
151+
}
152+
// Check if protocol calls for additional specifications
153+
if (in_array($protocol, array("tcp", "udp", "tcp/udp"))) {
154+
$port_req = true;
155+
}
156+
// Check our src and dst port values if ports are required
157+
if ($port_req) {
158+
foreach (array("source" => $srcport, "destination" => $dstport) as $dir => $val) {
159+
$val = str_replace("-", ":", $val);
160+
if (!is_port_or_range($val) and $val !== "any") {
161+
if ($dir === "source") {
162+
$this->errors[] = APIResponse\get(4013);
163+
164+
} else {
165+
$this->errors[] = APIResponse\get(4014);
166+
}
167+
} elseif ($val !== "any") {
168+
$this->validated_data[$dir]["port"] = str_replace(":", "-", $val);
169+
}
170+
}
171+
}
172+
}
173+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
require_once("api/framework/APIBaseModel.inc");
3+
require_once("api/framework/APIResponse.inc");
4+
5+
class APIFirewallNatPortForwardsDelete extends APIBaseModel {
6+
# Create our method constructor
7+
public function __construct() {
8+
parent::__construct();
9+
$this->methods = ["POST"];
10+
$this->privileges = ["page-all", "page-nat-portforward-edit"];
11+
}
12+
13+
public function action() {
14+
global $config;
15+
$_SESSION["Username"] = $this->client->username; // Save our CLIENT ID to session data for logging
16+
$change_note = " Deleted NAT rule via API"; // Add a change note
17+
$del_rule = $config["nat"]["rule"][$this->validated_data["id"]]; // Save the rule we are deleting
18+
unset($config["nat"]["rule"][$this->validated_data["id"]]); // Remove rule from our config
19+
APITools\sort_nat_rules(); // Sort our NAT rules
20+
write_config(sprintf(gettext($change_note))); // Apply our configuration change
21+
filter_configure(); // Ensure our firewall filter is reloaded
22+
return APIResponse\get(0, $del_rule);
23+
}
24+
25+
public function validate_payload() {
26+
global $config;
27+
if (isset($this->initial_data['id'])) {
28+
// Check that our rule ID exists
29+
if (array_key_exists($this->initial_data['id'], $config["nat"]["rule"])) {
30+
$this->validated_data["id"] = $this->initial_data['id'];
31+
} else {
32+
$this->errors[] = APIResponse\get(4016);
33+
}
34+
} else {
35+
$this->errors[] = APIResponse\get(4015);
36+
}
37+
}
38+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
require_once("api/framework/APIBaseModel.inc");
3+
require_once("api/framework/APIResponse.inc");
4+
5+
6+
class APIFirewallRules extends APIBaseModel {
7+
# Create our method constructor
8+
public function __construct() {
9+
parent::__construct();
10+
$this->methods = ["GET"];
11+
$this->privileges = ["page-all", "page-firewall-rules"];
12+
}
13+
14+
public function action() {
15+
global $config;
16+
// Check that we have a configuration
17+
if (!empty($config["filter"]["rule"])) {
18+
$rule_array = $config["filter"]["rule"];
19+
} else {
20+
$rule_array = [];
21+
}
22+
return APIResponse\get(0, $rule_array);
23+
}
24+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
require_once("api/framework/APIBaseModel.inc");
3+
require_once("api/framework/APIResponse.inc");
4+
5+
6+
class APIFirewallVirtualIPs extends APIBaseModel {
7+
# Create our method constructor
8+
public function __construct() {
9+
parent::__construct();
10+
$this->methods = ["GET"];
11+
$this->privileges = ["page-all", "page-firewall-virtualipaddresses"];
12+
}
13+
14+
public function action() {
15+
global $config;
16+
// Check that we have a virtual IP configuration
17+
if (!empty($config["virtualip"]["vip"])) {
18+
$vip_array = $config["virtualip"]["vip"];
19+
} else {
20+
$vip_array = [];
21+
}
22+
return APIResponse\get(0, $vip_array);
23+
}
24+
}

0 commit comments

Comments
 (0)