diff --git a/mkdocs.yml b/mkdocs.yml index d5097545..fca1f06b 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,4 +1,5 @@ site_name: pfSense REST API Guide +repo_url: https://github.com/jaredhendrickson13/pfsense-api nav: - General: - Home: index.md diff --git a/package-lock.json b/package-lock.json index eac56056..c7c4b406 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,7 +6,8 @@ "": { "devDependencies": { "@prettier/plugin-php": "^0.24.0", - "@stoplight/spectral-cli": "^6.15.0" + "@stoplight/spectral-cli": "^6.15.0", + "prettier": "^3.6.2" } }, "node_modules/@asyncapi/specs": { @@ -2154,11 +2155,11 @@ } }, "node_modules/prettier": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.4.tgz", - "integrity": "sha512-FWu1oLHKCrtpO1ypU6J0SbK2d9Ckwysq6bHj/uaCP26DxrPpppCLQRGVuqAxSTvhF00AcvDRyYrLNW7ocBhFFQ==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, - "peer": true, + "license": "MIT", "bin": { "prettier": "bin/prettier.cjs" }, diff --git a/package.json b/package.json index 990f4f6e..20dfe64c 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,6 @@ { "devDependencies": { + "prettier": "^3.6.2", "@prettier/plugin-php": "^0.24.0", "@stoplight/spectral-cli": "^6.15.0" } diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Fields/ForeignModelField.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Fields/ForeignModelField.inc index 0a5d8f32..6020a752 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Fields/ForeignModelField.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Fields/ForeignModelField.inc @@ -222,6 +222,11 @@ class ForeignModelField extends Field { $internal_value = $this->models[0]->{$this->model_field_internal}->_from_internal($internal_value); } + # If the model_field_internal, and the model_field are the same, return the internal value as-is. + if ($this->model_field_internal === $this->model_field) { + return $internal_value; + } + # Query for the Model object this value relates to. $query_modelset = $this->__get_matches($this->model_field_internal, $internal_value); diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/VirtualIP.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/VirtualIP.inc index 020342fb..5b2ce29f 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/VirtualIP.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/VirtualIP.inc @@ -10,6 +10,7 @@ use RESTAPI\Fields\IntegerField; use RESTAPI\Fields\InterfaceField; use RESTAPI\Fields\StringField; use RESTAPI\Fields\UIDField; +use RESTAPI\Responses\ConflictError; use RESTAPI\Responses\ValidationError; use RESTAPI\Validators\IPAddressValidator; use RESTAPI\Validators\UniqueFromForeignModelValidator; @@ -89,7 +90,6 @@ class VirtualIP extends Model { ); $this->vhid = new IntegerField( required: true, - unique: true, minimum: 1, maximum: 255, conditions: ['mode' => 'carp'], @@ -178,6 +178,27 @@ class VirtualIP extends Model { return $subnet_bits; } + /** + * Adds extra validation to the vhid field. + * @param int $vhid The incoming `vhid` value to be validated. + * @return int The validated `vhid` value to be set. + * @throws ValidationError When the `vhid` value is already used by another CARP virtual IP on the same interface. + */ + public function validate_vhid(int $vhid): int { + # Check for an existing CARP virtual IP with the same VHID on this interface + $vip_q = $this->query(id__except: $this->id, mode: 'carp', interface: $this->interface->value, vhid: $vhid); + + # Ensure no other CARP virtual IP on this interface is using the same VHID + if ($vip_q->exists()) { + $vip = $vip_q->first(); + throw new ConflictError( + message: "Virtual IP with ID '$vip->id' is already using VHID '$vhid' on interface '{$this->interface->value}'", + response_id: 'VIRTUALIP_VHID_ALREADY_IN_USE', + ); + } + return $vhid; + } + /** * Obtains the current internal CARP status of this object * @return string|null Returns a string that indicates the current CARP status of this virtual IP, or null diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsFirewallAliasTestCase.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsFirewallAliasTestCase.inc index c98d8e42..94a98405 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsFirewallAliasTestCase.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsFirewallAliasTestCase.inc @@ -3,13 +3,15 @@ namespace RESTAPI\Tests; use RESTAPI\Core\TestCase; +use RESTAPI\Core\TestCaseRetry; use RESTAPI\Models\FirewallAlias; class APIModelsFirewallAliasTestCase extends TestCase { /** * Checks that aliases with hostnames correctly populate a pfctl table */ - public function test_fqdn_alias_populates_pfctl_table() { + #[TestCaseRetry(retries: 3, delay: 1)] + public function test_fqdn_alias_populates_pfctl_table(): void { # Create an alias that includes dns.google as an alias item $test_alias = new FirewallAlias( data: [ diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsTableTestCase.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsTableTestCase.inc index 45dee568..f95abac4 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsTableTestCase.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsTableTestCase.inc @@ -82,6 +82,7 @@ class APIModelsTableTestCase extends TestCase { /** * Checks that we can successfully delete (flush) entrries from a table */ + #[TestCaseRetry(retries: 3, delay: 1)] public function test_delete(): void { # Create a new pf table to test with $this->add_table(table_name: 'pfrest_test_table', entries: ['1.2.3.4', '4.3.2.1']); diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsVirtualIPTestCase.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsVirtualIPTestCase.inc index bbf7b36b..1d594a41 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsVirtualIPTestCase.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsVirtualIPTestCase.inc @@ -242,4 +242,45 @@ class APIModelsVirtualIPTestCase extends TestCase { $carp_status->update(); $carp_vip->delete(apply: true); } + + public function test_carp_vhid_must_be_unique_per_interface(): void { + # Create a virtual IP to test with + $vip = new VirtualIP( + mode: 'carp', + interface: 'lan', + subnet: '127.1.2.3', + subnet_bits: 32, + password: 'testpasswd', + vhid: 5, + ); + $vip->create(); + + # Ensure we can update the existing VIP with the same VHID without issue + $this->assert_does_not_throw( + callable: function () use ($vip) { + $vip->validate_vhid(vhid: 5); + }, + ); + + # Ensure we cannot create a new VIP with the same VHID on the same interface + $this->assert_throws_response( + response_id: 'VIRTUALIP_VHID_ALREADY_IN_USE', + code: 409, + callable: function () { + $vip = new VirtualIP(mode: 'carp', interface: 'lan'); + $vip->validate_vhid(vhid: 5); + }, + ); + + # Ensure we can create a new VIP with the same VHID on a different interface + $this->assert_does_not_throw( + callable: function () { + $vip = new VirtualIP(mode: 'carp', interface: 'wan'); + $vip->validate_vhid(vhid: 5); + }, + ); + + # Clean up the VIP we created + $vip->delete(); + } }