Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
be9918b
feat: Add System HA Sync endpoint (#901)
TechAsen Jun 6, 2026
acdc050
Merge branch 'master' of github.com:jaredhendrickson13/pfsense-api in…
jaredhendrickson13 Jul 23, 2026
a2d8bfd
style: run prettier on changed files
jaredhendrickson13 Jul 23, 2026
52e75e1
fix(RESTAPISettingsSync): re-validate incoming settings sync-set data
jaredhendrickson13 Jul 27, 2026
dee8a0e
feat(Endpoint): allow endpoints to disable scoped privileges and enfo…
jaredhendrickson13 Jul 27, 2026
a30b280
fix: ensure /api/v2/system/restapi/settings/sync requires page-all
jaredhendrickson13 Jul 27, 2026
d2553a5
fix(RESTAPISettingsSync): reject sync requests when ha sync is disabled
jaredhendrickson13 Jul 27, 2026
21060f5
chore: do not remove ha sync flags
jaredhendrickson13 Jul 27, 2026
cc0c2f1
test: include e2e tests for rest api ha sync endpoint
jaredhendrickson13 Jul 27, 2026
64e1e43
test: update RESTAPISettingsSync model tests to accommodate changes
jaredhendrickson13 Jul 27, 2026
5248fc0
test: include Endpoint tests to cover new privilege generation gate
jaredhendrickson13 Jul 27, 2026
9e504cc
chore: remove leftover var dump from Endpoint test case
jaredhendrickson13 Jul 27, 2026
0042059
style: run prettier on changed files
jaredhendrickson13 Jul 27, 2026
f7021a6
docs: add note that ha sync requires page-all privileges
jaredhendrickson13 Jul 27, 2026
e4f3ff7
style: run prettier on changed files
jaredhendrickson13 Jul 27, 2026
38896a3
Merge branch 'next_patch' of github.com:jaredhendrickson13/pfsense-ap…
jaredhendrickson13 Jul 28, 2026
f3fe067
chore: cleanup HASync model for consistency
jaredhendrickson13 Jul 28, 2026
440942e
test: add tests for HASync model
jaredhendrickson13 Jul 28, 2026
81eb3c2
chore: cleanup SystemHASyncEndpoint definition for consistency
jaredhendrickson13 Jul 28, 2026
fb65fd6
chore(SystemDNS): sort parameters
jaredhendrickson13 Jul 28, 2026
73adfce
feat: add WireGuard status endpoints #790
jaredhendrickson13 Jul 28, 2026
27e7fd9
fix(HASync): allow loopback interfaces
jaredhendrickson13 Jul 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Endpoint.inc
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,13 @@ class Endpoint {
*/
public array $delete_privileges = [];

/**
* @var bool $requires_page_all_privilege
* When enabled, this endpoint will always require the page-all privilege and will
* disable auto-generated/scoped REST API privileges for this endpoint.
*/
public bool $requires_page_all_privilege = false;

/**
* @var string $get_help_text
* Sets the GET request's OpenAPI documentation for this Endpoint. This will be
Expand Down Expand Up @@ -534,6 +541,17 @@ class Endpoint {
*/
private function get_default_privs(): void {
$page_all_priv = 'page-all';

# If this endpoint requires page-all, then only assign page-all to the privileges for each method
if ($this->requires_page_all_privilege) {
$this->get_privileges = [$page_all_priv];
$this->post_privileges = [$page_all_priv];
$this->patch_privileges = [$page_all_priv];
$this->put_privileges = [$page_all_priv];
$this->delete_privileges = [$page_all_priv];
return;
}

$this->get_privileges = [$page_all_priv, $this->get_method_priv_name('GET')];
$this->post_privileges = [$page_all_priv, $this->get_method_priv_name('POST')];
$this->patch_privileges = [$page_all_priv, $this->get_method_priv_name('PATCH')];
Expand Down Expand Up @@ -564,6 +582,11 @@ class Endpoint {
* @returns array The pfSense priv list entry array corresponding to the privileges of this Endpoint.
*/
public function generate_pfsense_privs(): array {
# If this endpoint requires page-all, do not generate privileges
if ($this->requires_page_all_privilege) {
return [];
}

# Set an array to populate pfSense priv entries for this Endpoint
$privs = [];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1769,7 +1769,7 @@ class Model {
* @throws NotFoundError When the Model requires a pfSense package that is not installed.
* @throws ServerError When a package requires a PHP include file that could not be found.
*/
private function check_packages(): void {
protected function check_packages(): void {
# Check if the user has opted in to using development (-devel) package variants
$pkg_config = RESTAPI\Models\RESTAPISettings::get_pkg_config();
$allow_development_packages = ($pkg_config['allow_development_packages'] ?? '') === 'enabled';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

namespace RESTAPI\Endpoints;

require_once 'RESTAPI/autoloader.inc';

use RESTAPI\Core\Endpoint;

/**
* Defines an Endpoint for interacting with multiple WireGuard peer status objects at /api/v2/status/wireguard/peers.
*/
class StatusWireGuardPeersEndpoint extends Endpoint {
public function __construct() {
# Set Endpoint attributes
$this->url = '/api/v2/status/wireguard/peers';
$this->model_name = 'WireGuardPeerStatus';
$this->many = true;
$this->request_method_options = ['GET'];

# Construct the parent Endpoint object
parent::__construct();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

namespace RESTAPI\Endpoints;

require_once 'RESTAPI/autoloader.inc';

use RESTAPI\Core\Endpoint;

/**
* Defines an Endpoint for interacting with multiple WireGuard tunnel status objects at /api/v2/status/wireguard/tunnels.
*/
class StatusWireGuardTunnelsEndpoint extends Endpoint {
public function __construct() {
# Set Endpoint attributes
$this->url = '/api/v2/status/wireguard/tunnels';
$this->model_name = 'WireGuardTunnelStatus';
$this->many = true;
$this->request_method_options = ['GET'];

# Construct the parent Endpoint object
parent::__construct();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

namespace RESTAPI\Endpoints;

require_once 'RESTAPI/autoloader.inc';

use RESTAPI\Core\Endpoint;

/**
* Defines an Endpoint for interacting with pfSense High Availability synchronization settings.
*/
class SystemHASyncEndpoint extends Endpoint {
public function __construct() {
# Set Endpoint attributes
$this->url = '/api/v2/system/hasync';
$this->model_name = 'HASync';
$this->request_method_options = ['GET', 'PATCH'];

$this->get_help_text = 'Reads pfSense High Availability synchronization settings.';
$this->patch_help_text = 'Updates pfSense High Availability synchronization settings.';

parent::__construct();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class SystemRESTAPISettingsSyncEndpoint extends Endpoint {
$this->ignore_interfaces = true;
$this->ignore_read_only = true;
$this->auth_methods = ['BasicAuth'];
$this->requires_page_all_privilege = true;

# Construct the parent Endpoint object
parent::__construct();
Expand Down
241 changes: 241 additions & 0 deletions pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/HASync.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
<?php

namespace RESTAPI\Models;

require_once 'RESTAPI/autoloader.inc';

use RESTAPI\Core\Model;
use RESTAPI\Dispatchers\FirewallApplyDispatcher;
use RESTAPI\Fields\BooleanField;
use RESTAPI\Fields\InterfaceField;
use RESTAPI\Fields\StringField;
use RESTAPI\Validators\IPAddressValidator;

/**
* Defines a Model for managing pfSense High Availability synchronization settings.
*/
class HASync extends Model {
public StringField $synchronizetoip;
public StringField $pfsyncpeerip;
public InterfaceField $pfsyncinterface;
public StringField $pfhostid;
public StringField $username;
public StringField $password;
public BooleanField $pfsyncenabled;
public BooleanField $adminsync;
public BooleanField $synchronizeusers;
public BooleanField $synchronizeauthservers;
public BooleanField $synchronizecerts;
public BooleanField $synchronizerules;
public BooleanField $synchronizeschedules;
public BooleanField $synchronizealiases;
public BooleanField $synchronizenat;
public BooleanField $synchronizeipsec;
public BooleanField $synchronizeopenvpn;
public BooleanField $synchronizedhcpd;
public BooleanField $synchronizedhcpdv6;
public BooleanField $synchronizekea6;
public BooleanField $synchronizewol;
public BooleanField $synchronizestaticroutes;
public BooleanField $synchronizevirtualip;
public BooleanField $synchronizetrafficshaper;
public BooleanField $synchronizetrafficshaperlimiter;
public BooleanField $synchronizednsforwarder;
public BooleanField $synchronizecaptiveportal;
public BooleanField $synchronizedhcrelay;
public BooleanField $synchronizedhcrelay6;

public function __construct(mixed $id = null, mixed $parent_id = null, mixed $data = [], mixed ...$options) {
# Set Model attributes
$this->config_path = 'hasync';
$this->many = false;
$this->always_apply = true;
$this->verbose_name = 'HA Sync Settings';
$this->verbose_name_plural = 'HA Sync Settings';

$this->pfsyncenabled = new BooleanField(
default: false,
indicates_true: 'on',
indicates_false: null,
help_text: 'Enable pfsync state synchronization.',
);
$this->pfsyncinterface = new InterfaceField(
default: 'lo0',
allow_localhost_interface: true,
allow_empty: true,
help_text: 'The interface used by pfsync state synchronization.',
);
$this->pfhostid = new StringField(
default: '',
allow_empty: true,
maximum_length: 8,
help_text: 'Custom pf host identifier carried in state data.',
);
$this->pfsyncpeerip = new StringField(
default: '',
allow_empty: true,
validators: [new IPAddressValidator(allow_ipv4: true, allow_ipv6: false)],
help_text: 'The peer IP address used by pfsync.',
);
$this->synchronizetoip = new StringField(
default: '',
allow_empty: true,
validators: [new IPAddressValidator(allow_ipv4: true, allow_ipv6: false)],
help_text: 'The remote pfSense host IP address used for XMLRPC configuration synchronization.',
);
$this->username = new StringField(
default: '',
allow_empty: true,
help_text: 'The remote pfSense username used for XMLRPC synchronization.',
);
$this->password = new StringField(
default: '',
allow_empty: true,
write_only: true,
sensitive: true,
internal_name: 'passwordfld',
help_text: 'The remote pfSense password used for XMLRPC synchronization.',
);
$this->adminsync = new BooleanField(
default: false,
indicates_true: 'on',
indicates_false: null,
help_text: 'Synchronize admin accounts and automatically update the XMLRPC sync password.',
);
$this->synchronizeusers = new BooleanField(
default: false,
indicates_true: 'on',
indicates_false: null,
help_text: 'Synchronize users and groups.',
);
$this->synchronizeauthservers = new BooleanField(
default: false,
indicates_true: 'on',
indicates_false: null,
help_text: 'Synchronize authentication servers.',
);
$this->synchronizecerts = new BooleanField(
default: false,
indicates_true: 'on',
indicates_false: null,
help_text: 'Synchronize certificates.',
);
$this->synchronizerules = new BooleanField(
default: false,
indicates_true: 'on',
indicates_false: null,
help_text: 'Synchronize firewall rules.',
);
$this->synchronizeschedules = new BooleanField(
default: false,
indicates_true: 'on',
indicates_false: null,
help_text: 'Synchronize firewall schedules.',
);
$this->synchronizealiases = new BooleanField(
default: false,
indicates_true: 'on',
indicates_false: null,
help_text: 'Synchronize firewall aliases.',
);
$this->synchronizenat = new BooleanField(
default: false,
indicates_true: 'on',
indicates_false: null,
help_text: 'Synchronize NAT configuration.',
);
$this->synchronizeipsec = new BooleanField(
default: false,
indicates_true: 'on',
indicates_false: null,
help_text: 'Synchronize IPsec configuration.',
);
$this->synchronizeopenvpn = new BooleanField(
default: false,
indicates_true: 'on',
indicates_false: null,
help_text: 'Synchronize OpenVPN configuration.',
);
$this->synchronizedhcpd = new BooleanField(
default: false,
indicates_true: 'on',
indicates_false: null,
help_text: 'Synchronize DHCP server configuration.',
);
$this->synchronizedhcpdv6 = new BooleanField(
default: false,
indicates_true: 'on',
indicates_false: null,
help_text: 'Synchronize DHCPv6 server configuration.',
);
$this->synchronizekea6 = new BooleanField(
default: false,
indicates_true: 'on',
indicates_false: null,
help_text: 'Synchronize Kea DHCPv6 server configuration.',
);
$this->synchronizedhcrelay = new BooleanField(
default: false,
indicates_true: 'on',
indicates_false: null,
help_text: 'Synchronize DHCP relay configuration.',
);
$this->synchronizedhcrelay6 = new BooleanField(
default: false,
indicates_true: 'on',
indicates_false: null,
help_text: 'Synchronize DHCPv6 relay configuration.',
);
$this->synchronizewol = new BooleanField(
default: false,
indicates_true: 'on',
indicates_false: null,
help_text: 'Synchronize Wake-on-LAN configuration.',
);
$this->synchronizestaticroutes = new BooleanField(
default: false,
indicates_true: 'on',
indicates_false: null,
help_text: 'Synchronize static routes.',
);
$this->synchronizevirtualip = new BooleanField(
default: false,
indicates_true: 'on',
indicates_false: null,
help_text: 'Synchronize virtual IP addresses.',
);
$this->synchronizetrafficshaper = new BooleanField(
default: false,
indicates_true: 'on',
indicates_false: null,
help_text: 'Synchronize traffic shaper queues.',
);
$this->synchronizetrafficshaperlimiter = new BooleanField(
default: false,
indicates_true: 'on',
indicates_false: null,
help_text: 'Synchronize traffic shaper limiters.',
);
$this->synchronizednsforwarder = new BooleanField(
default: false,
indicates_true: 'on',
indicates_false: null,
help_text: 'Synchronize DNS Forwarder and DNS Resolver configuration.',
);
$this->synchronizecaptiveportal = new BooleanField(
default: false,
indicates_true: 'on',
indicates_false: null,
help_text: 'Synchronize captive portal configuration.',
);

parent::__construct($id, $parent_id, $data, ...$options);
}

/**
* Applies HA Sync configuration changes via the FirewallApplyDispatcher.
*/
public function apply(): void {
(new FirewallApplyDispatcher(async: $this->async))->spawn_process();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ class RESTAPISettings extends Model {
allow_empty: true,
verbose_name: 'HA username',
help_text: "Sets the username to use when authenticating for HA sync processes. This user must be the present
on all hosts defined in `ha_sync_hosts`.",
on all hosts defined in `ha_sync_hosts` and must hold the `page-all` privilege",
);
$this->ha_sync_password = new StringField(
default: '',
Expand Down
Loading