diff --git a/.github/workflows/schedule_weekly.yml b/.github/workflows/schedule_weekly.yml new file mode 100644 index 000000000..9aecee777 --- /dev/null +++ b/.github/workflows/schedule_weekly.yml @@ -0,0 +1,26 @@ +name: Weekly Schedule + +on: + workflow_dispatch: # Enable manual triggering + schedule: + - cron: "0 18 * * 1" + +permissions: + issues: write + pull-requests: write + +jobs: + stale: + runs-on: ubuntu-latest + env: + DAYS_BEFORE_STALE: 60 + steps: + - uses: actions/stale@v9 + with: + stale-issue-message: | + This issue has been automatically marked as stale because it has had no recent activity in the last ${{ env.DAYS_BEFORE_STALE }} days. + stale-pr-message: | + This pull request has been automatically marked as stale because it has had no recent activity in the last ${{ env.DAYS_BEFORE_STALE }} days. + days-before-stale: ${{ env.DAYS_BEFORE_STALE }} + days-before-close: 0 + exempt-issue-labels: "feature request,enhancement,bug" diff --git a/README.md b/README.md index bb759714c..bcc675b9c 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ ![Docs](https://img.shields.io/website?url=https%3A%2F%2Fpfrest.org&label=Documentation) The pfSense REST API package is an unofficial, open-source REST and GraphQL API for pfSense CE and pfSense Plus -firewalls.It is designed to be light-weight, fast, and easy to use. This guide will help you get started with the REST +firewalls. It is designed to be light-weight, fast, and easy to use. This guide will help you get started with the REST API package and provide you with the information you need to configure and use the package effectively. ## Key Features diff --git a/docs/INSTALL_AND_CONFIG.md b/docs/INSTALL_AND_CONFIG.md index 699518b68..0cd0a40ae 100644 --- a/docs/INSTALL_AND_CONFIG.md +++ b/docs/INSTALL_AND_CONFIG.md @@ -47,7 +47,9 @@ pkg-static -C /dev/null add https://github.com/jaredhendrickson13/pfsense-api/re the [releases page](https://github.com/jaredhendrickson13/pfsense-api/releases) to find the package built for your version of pfSense. - When updating pfSense, **you must reinstall this package afterward** as pfSense removes unofficial packages during - system updates and has no way to automatically reinstall them. + system updates and has no way to automatically reinstall them. + - If you're looking for a method of programmatically installing the package without SSH, check out + [pfsense-vshell](https://github.com/jaredhendrickson13/pfsense-vshell)! ## Configuring the package @@ -70,16 +72,60 @@ pfsense-restapi delete ## Updating the package +Before updating the package, it is recommended to enable the REST API's 'Keep Backup' setting to ensure that your +REST API configurations, keys and access lists are not lost during the update process. It is also highly recommended to +read and understand both the release change notes on GitHub and the [versioning policy](#versioning-policy) to ensure +you do not unintentionally introduce breaking changes to your environment. + +!!! Tip + While the package is updating, the REST API may be unavailable for a short period of time. Updates typically complete + within a minute, but may vary depending on network environment and conditions. It is recommended to + schedule updates during off-peak hours to minimize impact to your integrations. In the event that the update fails, + or takes an excessive amount of time, it is recommended to uninstall and reinstall the package. + +### From the pfSense webConfigurator + +You can easily update or revert the package version from the pfSense webConfigurator by navigating to 'System' -> +'REST API' -> 'Updates' and select the desired version. Click 'Save' to apply the desired version. + +### From the API + +You can update or revert the package to a specified version by sending a request to the [PATCH +/api/v2/system/restapi/version](https://pfrest.org/api-docs/#/SYSTEM/patchSystemRESTAPIVersionEndpoint) endpoint. +Simply set the `install_version` field to the desired version and send the request. + +### From the command line + You can update the package to latest version available to your pfSense version by running the following command: ```bash pfsense-restapi update ``` -## Reverting the package to a specific version - -If you need to revert or upgrade the package to a specific version, you can do so by running the following command: +If you need to revert or upgrade the package to a _specific_ version, you can do so by running the following command: ```bash pfsense-restapi revert ``` + +## Versioning policy + +The REST API package loosely follows [Semantic Versioning](https://semver.org/). The versioning policy is as follows: + +- Major version changes (e.g. 1.x.x to 2.x.x) may include major breaking changes and are not guaranteed to be backwards + compatible. Major changes will often include significant changes to the framework, endpoints, schemas, and/or behavior. +- Minor version changes (e.g. 2.0.x to 2.1.x) may include new features, bug fixes, and/or minor breaking changes. Breaking + changes will be isolated to specific endpoints and will be documented in the release notes. +- Patch version changes (e.g. 2.0.0 to 2.0.1) will only include bug fixes and very small enhancements. Patches will + never contain breaking changes or significant new features that could impact existing functionality. + +### Pre-release versions + +Pre-release versions are occasionally made available to the public to allow for testing of new features and fixes. +Pre-releases will be notated as such on GitHub and are not considered as release candidates within the REST API package's +update features by default. You may opt-in to pre-release updates by enabling the 'Allow Pre-releases' setting in the +REST API settings. + +!!! Warning + Pre-release versions may contain breaking changes, bugs, and/or incomplete features. It is recommended to only use + pre-release versions in testing environments and never in production. \ No newline at end of file diff --git a/docs/SECURITY.md b/docs/SECURITY.md index e0249a66d..f99c78f8d 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -1,21 +1,29 @@ # Security Policy -## Supported Versions +Security is a top priority for this project. The REST API package is designed to be secure and provide granular controls +to help admins implement a multi-layered security approach to API access. This document outlines the security policy +for the project and provides information on how to report a security vulnerability. -Below are versions that are currently supported and will receive security updates when available. +## Supported Versions -| Version | Supported | -|---------| ------------------ | -| 2.2.x | :white_check_mark: | -| 1.7.x | :white_check_mark: | -| <=1.6.x | :x: | +Currently, there are two supported versions of the package: the v2 package (pfSense-pkg-RESTAPI) and the legacy v1 +package (pfSense-pkg-API). The v2 package is the latest version of the package and is actively developed and fully +maintained. The legacy v1 package is no longer actively developed and is only receiving compatibility fixes and critical +security updates when necessary. It is highly recommended to regularly update to the latest version of the package to +ensure you are receiving important bug fixes and security updates. ## Reporting a Vulnerability -Should you discover a vulnerability in the pfSense-API code, please report the issue in one of the following ways: +Should you discover a vulnerability in the codebase, please report the issue using the order of preference below: 1. A pull request with code that fixes the discovered vulnerability 2. A private email to either the project owner or the respective code owner 3. As a last resort, you may open a public issue on the repository -Please note this is an independent and open-source project and no bug bounty or reward can be granted. +Please do not disclose the details of the vulnerability publicly until it has been addressed by the maintainers. +The maintainers will work to address the vulnerability as quickly as possible and will provide updates on the issue +as they become available. + +!!! Notice + This is an open-source project and is developed and maintained by members of the pfSense community. No bug bounty + can be offered for reported vulnerabilities. diff --git a/docs/index.md b/docs/index.md index 02c93525d..7e4502f29 100644 --- a/docs/index.md +++ b/docs/index.md @@ -40,9 +40,9 @@ opening a pull request. - Jared Hendrickson Jared Hendrickson - github@jaredhendrickson.com !!! Important - Unless your inquiry is regarding a security vulnerability or other sensitive matter, please do not contact the - maintainers directly. Instead, please [open an issue](https://github.com/jaredhendrickson13/pfsense-api/issues/new/choose) - to report a bug or request a feature. For general questions or help requests, please [open a discussion](https://github.com/jaredhendrickson13/pfsense-api/discussions/new/choose). + Unless your inquiry is regarding a [security vulnerability](SECURITY.md) or other sensitive matter, please do not + contact the maintainers directly. Instead, please [open an issue](https://github.com/jaredhendrickson13/pfsense-api/issues/new/choose) to report a bug or request a feature. For + general questions or help requests, please [open a discussion](https://github.com/jaredhendrickson13/pfsense-api/discussions/new/choose). ## Disclaimers diff --git a/mkdocs.yml b/mkdocs.yml index d4c930e79..2c1ee0ce0 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -17,6 +17,7 @@ nav: - Advanced Topics: - Introduction: ADVANCED_TOPICS_INTRO.md - Contributing & Development: CONTRIBUTING.md + - Security Policy: SECURITY.md - Building Custom Authentication: BUILDING_CUSTOM_AUTH_CLASSES.md - Building Custom Query Filters: BUILDING_CUSTOM_QUERY_FILTER_CLASSES.md - Building Custom Models: BUILDING_CUSTOM_MODEL_CLASSES.md diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Endpoint.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Endpoint.inc index ccbc0482d..16aa82cf9 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Endpoint.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Endpoint.inc @@ -1147,8 +1147,10 @@ class Endpoint { * Creates a new object for the assigned Model using the data submitted in a POST request. */ protected function post(): Model|ModelSet { - # POST request cannot include an ID, strip the ID if present - unset($this->request_data['id']); + # POST requests cannot include an ID unless auto_create_id is off + if ($this->model->auto_create_id) { + unset($this->request_data['id']); + } # Construct the model from representation using the client's request data $this->model->from_representation(data: $this->request_data); diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Model.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Model.inc index b491f2d81..0bec530c1 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Model.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Model.inc @@ -89,6 +89,15 @@ class Model { */ public string $id_type = 'integer'; + /** + * @var bool $auto_create_id + * Enables or disables automatic creation of an ID for this Model object when the `create()` method is called. If + * set to `true`, the ID will be automatically generated. If set to `false`, an ID must be provided before the + * `create()` method is called. This applies exclusively to Models with $many enabled and is usually only + * relevant to Model classes that use an internal callable method. + */ + public bool $auto_create_id = true; + /** * @var mixed $parent_id * For Models acting as children to a different Model class, this property will contain the ID of the parent model @@ -2036,7 +2045,7 @@ class Model { */ final public function create(bool $apply = false): Model { # Ensure all object Fields and validations succeed for proceeding. - if ($this->validate(create_id: true)) { + if ($this->validate(requires_id: !$this->auto_create_id, create_id: $this->auto_create_id)) { # Check if creating this object would put the total number of objects over the $many_maximum $this->check_many_maximum(); diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Endpoints/ServicesDHCPRelayEndpoint.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Endpoints/ServicesDHCPRelayEndpoint.inc new file mode 100644 index 000000000..ac0cd4de7 --- /dev/null +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Endpoints/ServicesDHCPRelayEndpoint.inc @@ -0,0 +1,22 @@ +url = '/api/v2/services/dhcp_relay'; + $this->model_name = 'DHCPRelay'; + $this->request_method_options = ['GET', 'PATCH']; + + # Construct the parent Endpoint object + parent::__construct(); + } +} diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Endpoints/ServicesHAProxyFrontendCertificateEndpoint.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Endpoints/ServicesHAProxyFrontendCertificateEndpoint.inc new file mode 100644 index 000000000..06e53282e --- /dev/null +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Endpoints/ServicesHAProxyFrontendCertificateEndpoint.inc @@ -0,0 +1,23 @@ +url = '/api/v2/services/haproxy/frontend/certificate'; + $this->model_name = 'HAProxyFrontendCertificate'; + $this->request_method_options = ['GET', 'POST', 'PATCH', 'DELETE']; + + # Construct the parent Endpoint object + parent::__construct(); + } +} diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/BINDZone.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/BINDZone.inc index 42921e8e6..f72481dff 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/BINDZone.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/BINDZone.inc @@ -35,10 +35,10 @@ class BINDZone extends Model { public StringField $nameserver; public StringField $mail; public IntegerField $serial; - public IntegerField $refresh; - public IntegerField $retry; - public IntegerField $expire; - public IntegerField $minimum; + public StringField $refresh; + public StringField $retry; + public StringField $expire; + public StringField $minimum; public BooleanField $enable_updatepolicy; public StringField $updatepolicy; public ForeignModelField $allowupdate; @@ -161,30 +161,34 @@ class BINDZone extends Model { conditions: ['type' => ['master', 'redirect']], help_text: 'The SOA serial number for this zone.', ); - $this->refresh = new IntegerField( + $this->refresh = new StringField( default: null, allow_null: true, conditions: ['type' => ['master', 'redirect']], - help_text: 'The SOA refresh interval (in seconds) for this zone.', + help_text: 'The SOA refresh interval for this zone. TTL-style time-unit suffixes are ' . + 'supported (e.g. 1h, 1d, 1w), otherwise time in seconds is assumed.', ); - $this->retry = new IntegerField( + $this->retry = new StringField( default: null, allow_null: true, conditions: ['type' => ['master', 'redirect']], - help_text: 'The SOA retry interval (in seconds) for this zone.', + help_text: 'The SOA retry interval for this zone. TTL-style time-unit suffixes are ' . + 'supported (e.g. 1h, 1d, 1w), otherwise time in seconds is assumed.', ); - $this->expire = new IntegerField( + $this->expire = new StringField( default: null, allow_null: true, conditions: ['type' => ['master', 'redirect']], - help_text: 'The SOA expiry interval (in seconds) for this zone.', + help_text: 'The SOA expiry interval for this zone. TTL-style time-unit suffixes are ' . + 'supported (e.g. 1h, 1d, 1w), otherwise time in seconds is assumed.', ); - $this->minimum = new IntegerField( + $this->minimum = new StringField( default: null, allow_null: true, conditions: ['type' => ['master', 'redirect']], help_text: 'The SOA minimum TTL interval (in seconds) for this zone. This is also referred to as the ' . - 'negative TTL.', + 'negative TTL. TTL-style time-unit suffixes are supported (e.g. 1h, 1d, 1w), otherwise time in ' . + 'seconds is assumed.', ); $this->enable_updatepolicy = new BooleanField( default: false, diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/DHCPRelay.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/DHCPRelay.inc new file mode 100644 index 000000000..59870b790 --- /dev/null +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/DHCPRelay.inc @@ -0,0 +1,109 @@ +config_path = 'dhcrelay'; + $this->many = false; + $this->always_apply = true; + + # Set model fields + $this->enable = new BooleanField(default: false, help_text: 'Enables or disables the DHCP relay.'); + $this->interface = new InterfaceField( + default: [], + many: true, + help_text: 'The downstream interfaces to listen on for DHCP requests.', + ); + $this->agentoption = new BooleanField( + default: false, + help_text: 'Enables or disables appending the circuit ID (interface number) and the agent ID to the ' . + 'DHCP request.', + ); + $this->carpstatusvip = new ForeignModelField( + model_name: 'VirtualIP', + model_field: 'uniqid', + allowed_keywords: ['none'], + default: 'none', + help_text: 'DHCP Relay will be stopped when the chosen VIP is in BACKUP status, and started ' . + 'in MASTER status.', + ); + $this->server = new StringField( + required: true, + many: true, + many_minimum: 1, + validators: [new IPAddressValidator(allow_ipv6: false)], + help_text: 'The IPv4 addresses of the DHCP server to relay requests to.', + ); + + parent::__construct($id, $parent_id, $data, ...$options); + } + + /** + * Provide extra validation to the 'enable' field. + * @param bool $enable The value to be validated. + * @return bool The validated value. + * @throws ConflictError If the DHCP relay is being enabled but a DHCP Server is running. + */ + public function validate_enable(bool $enable): bool { + if ($enable and DHCPServer::query(enable: true)->exists()) { + throw new ConflictError( + message: 'DHCP Relay cannot be enabled while DHCP Server is enabled on any interface.', + response_id: 'DHCP_RELAY_CANNOT_BE_ENABLED_WHILE_DHCP_SERVER_IS_ENABLED', + ); + } + + return $enable; + } + + /** + * Override the default 'to_internal()' method to ensure the carpstatusvip field is prefixed with '_vip'. + */ + public function to_internal(): array|string { + $internal_data = parent::to_internal(); + + if ($this->carpstatusvip->value !== 'none') { + $internal_data['carpstatusvip'] = "_vip{$internal_data['carpstatusvip']}"; + } + + return $internal_data; + } + + /** + * Add custom logic for when the 'carpstatusvip' field is being set from its internal value. This is used to + * remove the _vip prefix from the value so the framework can locate it's related VirtualIP model. + */ + public function from_internal_carpstatusvip(string $value): string { + if (str_starts_with($value, '_vip')) { + return substr($value, 4); + } + + return $value; + } + + /** + * Apply changes to the DHCP relay + */ + public function apply(): void { + services_dhcrelay_configure(); + filter_configure(); + } +} diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/DHCPServer.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/DHCPServer.inc index 636d418fc..4a0a18a50 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/DHCPServer.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/DHCPServer.inc @@ -315,15 +315,13 @@ class DHCPServer extends Model { ); } - # Do not allow the DHCP server to be enabled if this interface is already running a DHCP relay - # TODO: Replace this with a Model query when the DHCPRelay Model is developed - foreach ($this->get_config('dhcrelay', []) as $dhcp_relay) { - if ($this->id === $dhcp_relay['interface']) { - throw new ValidationError( - message: 'DHCP server cannot be enabled because a DHCP relay is already running.', - response_id: 'DHCP_SERVER_CANNOT_BE_ENABLED_WITH_DHCP_RELAY', - ); - } + # Do not allow the DHCP server to be enabled if any interface is running a DHCP relay + $dhcp_relay = new DHCPRelay(); + if ($dhcp_relay->enable->value) { + throw new ValidationError( + message: 'DHCP server cannot be enabled while the DHCP Relay is enabled.', + response_id: 'DHCP_SERVER_CANNOT_BE_ENABLED_WITH_DHCP_RELAY', + ); } return $enable; diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/HAProxyBackend.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/HAProxyBackend.inc index cbfda047d..47c460002 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/HAProxyBackend.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/HAProxyBackend.inc @@ -4,6 +4,7 @@ namespace RESTAPI\Models; use RESTAPI\Core\Model; use RESTAPI\Dispatchers\HAProxyApplyDispatcher; +use RESTAPI\Fields\Base64Field; use RESTAPI\Fields\BooleanField; use RESTAPI\Fields\IntegerField; use RESTAPI\Fields\InterfaceField; @@ -72,7 +73,7 @@ class HAProxyBackend extends Model { public NestedModelField $errorfiles; public BooleanField $cookie_attribute_secure; public StringField $advanced; - public StringField $advanced_backend; + public Base64Field $advanced_backend; public BooleanField $transparent_clientip; public InterfaceField $transparent_interface; @@ -439,7 +440,7 @@ class HAProxyBackend extends Model { allow_empty: true, help_text: 'The per server pass thru to apply to each server line.', ); - $this->advanced_backend = new StringField( + $this->advanced_backend = new Base64Field( default: '', allow_empty: true, help_text: 'The backend pass thru to apply to the backend section.', diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/HAProxyFrontend.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/HAProxyFrontend.inc index 5c1090fb1..90357d6bb 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/HAProxyFrontend.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/HAProxyFrontend.inc @@ -96,6 +96,19 @@ class HAProxyFrontend extends Model { allow_null: true, help_text: 'The default backend to use for this frontend.', ); + $this->ssloffloadcert = new ForeignModelField( + model_name: 'Certificate', + model_field: 'refid', + default: null, + allow_null: true, + help_text: 'The default SSL/TLS certificate refid to use for this frontend.', + ); + $this->ha_certificates = new NestedModelField( + model_class: 'HAProxyFrontendCertificate', + default: [], + allow_empty: true, + help_text: 'The additional SSL/TLS certificates to use on this frontend.', + ); $this->socket_stats = new BooleanField( default: false, indicates_true: 'yes', diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/HAProxyFrontendCertificate.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/HAProxyFrontendCertificate.inc new file mode 100644 index 000000000..0f2b5b396 --- /dev/null +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/HAProxyFrontendCertificate.inc @@ -0,0 +1,48 @@ +parent_model_class = 'HAProxyFrontend'; + $this->config_path = 'ha_certificates/item'; + $this->verbose_name = 'HAProxy Frontend Certificates'; + $this->packages = ['pfSense-pkg-haproxy']; + $this->package_includes = ['haproxy/haproxy.inc', 'haproxy/haproxy_utils.inc']; + $this->many = true; + + $this->ssl_certificate = new ForeignModelField( + model_name: 'Certificate', + model_field: 'refid', + default: null, + allow_null: true, + help_text: 'The SSL/TLS certificate refid to add to this frontend.', + ); + + parent::__construct($id, $parent_id, $data, ...$options); + } + + /** + * Ensures haproxy is marked as dirty before applying. + */ + public function pre_apply(): void { + touch('/var/run/haproxy.conf.dirty'); + } + + /** + * Applies changes to the HAProxy configuration. + */ + public function apply(): void { + (new HAProxyApplyDispatcher(async: $this->async))->spawn_process(); + } +} diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/Service.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/Service.inc index c47734694..a7b38aa51 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/Service.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/Service.inc @@ -20,20 +20,17 @@ class Service extends Model { public function __construct(mixed $id = null, mixed $parent_id = null, mixed $data = [], mixed ...$options) { # Set Model attributes $this->internal_callable = 'get_services'; + $this->auto_create_id = false; // We don't create services with create(), we just control existing ones $this->many = true; # Set Model Fields - $this->name = new StringField( - required: true, - choices_callable: 'get_service_name_choices', - help_text: 'The internal name of the service.', - ); $this->action = new StringField( required: true, choices: ['start', 'stop', 'restart'], write_only: true, help_text: 'The action to perform against this service.', ); + $this->name = new StringField(read_only: true, help_text: 'The internal name of the service.'); $this->description = new StringField(read_only: true, help_text: 'The full descriptive name of the service.'); $this->enabled = new BooleanField( read_only: true, @@ -57,59 +54,52 @@ class Service extends Model { * @return array An array of available pfSense services and their current statuses. */ public static function get_services(): array { - # Variables - $services = get_services(); - - # Loop through each service and set the proper values - foreach ($services as $id => $service) { - # Ensure the $enabled and $status values are always present - $services[$id]['enabled'] = is_service_enabled($service['name']); - $services[$id]['status'] = is_service_running($service['name']); - } - - return $services; - } - - /** - * Obtains all internal service choices for the `name` field. - * @return array An array of available service names. - */ - public function get_service_name_choices(): array { - $choices = []; - foreach ($this->get_services() as $service) { - $choices[] = $service['name']; - } - return $choices; - } - - /** - * Gets the representation ID for a given service name. - * @param string $name The internal service to obtain the ID for. - * @return int The representation ID of the service with this name. - */ - public function get_id_by_name(string $name): int { - return $this->query(['name' => $name])->first()->id; + return get_services(); } /** * Starts or stops a specified service. */ - public function _create() { + public function _create(): void { + # Obtain the extras for this service. Provides additional info required for some services to be controlled. + $extras = $this->_format_extra_data($this->get_services()[$this->id]); + # Trigger the requested service action switch ($this->action->value) { case 'start': - service_control_start(name: $this->name->value, extras: []); + service_control_start(name: $this->name->value, extras: $extras); break; case 'stop': - service_control_stop(name: $this->name->value, extras: []); + service_control_stop(name: $this->name->value, extras: $extras); break; case 'restart': - service_control_restart(name: $this->name->value, extras: []); + service_control_restart(name: $this->name->value, extras: $extras); break; } - # Populate the current status of the service with this name so this object is up-to-date. - $this->id = $this->get_id_by_name($this->name->value); + # Recheck the service status after the action has been performed to ensure the correct status is returned $this->from_internal(); } + + /** + * Formats the 'extra' service data required for some services to be restarted to the weird format pfSense + * sometimes expects. In the webConfigurator, this process is obfuscated by AJAX calls and JavaScript so there + * is no direct function to format this data. This function is a workaround to provide the correct data format. + * + * @param array $service The service data to format. + * @return array The formatted service data suitable for the service control functions 'extras' parameter. + */ + public function _format_extra_data(array $service): array { + # Format OpenVPN service extra data for service control + if ($service['name'] === 'openvpn') { + # 'mode' changes to 'vpnmode' + $service['vpnmode'] = $service['mode']; + unset($service['mode']); + + # 'vpnid' changes to just 'id' + $service['id'] = $service['vpnid']; + } + + return $service; + } } diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Schemas/OpenAPISchema.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Schemas/OpenAPISchema.inc index cb06afe2a..3e6754a68 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Schemas/OpenAPISchema.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Schemas/OpenAPISchema.inc @@ -184,7 +184,7 @@ class OpenAPISchema extends Schema { } # For non `many` Endpoints with `many` Models, add the ID to the schema and make it required - if (!$endpoint->many and $model->many and $request_method !== 'post') { + if (!$endpoint->many and $model->many and ($request_method !== 'post' or !$model->auto_create_id)) { $schema = [ 'schema' => [ 'allOf' => [ diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsBINDZoneTestCase.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsBINDZoneTestCase.inc index ee65c1316..ab01ceafb 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsBINDZoneTestCase.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsBINDZoneTestCase.inc @@ -49,10 +49,10 @@ class APIModelsBINDZoneTestCase extends TestCase { nameserver: 'ns1.example.com', mail: 'admin.example.com', serial: 123456, - refresh: 3605, - retry: 605, - expire: 86405, - minimum: 2605, + refresh: '3605', + retry: '605', + expire: '86405', + minimum: '2605', records: [ ['name' => 'a.example.com.', 'type' => 'A', 'rdata' => '4.3.2.1'], ['name' => 'mx.example.com.', 'type' => 'MX', 'rdata' => 'mail.example.com.', 'priority' => 5], @@ -92,10 +92,10 @@ class APIModelsBINDZoneTestCase extends TestCase { $zone->nameserver->value = 'ns2.example.com'; $zone->mail->value = 'admin2.example.com'; $zone->serial->value = 654321; - $zone->refresh->value = 3606; - $zone->retry->value = 606; - $zone->expire->value = 86406; - $zone->minimum->value = 2606; + $zone->refresh->value = '3606'; + $zone->retry->value = '606'; + $zone->expire->value = '86406'; + $zone->minimum->value = '2606'; $zone->records->value = [ ['name' => 'a2.example.com.', 'type' => 'A', 'rdata' => '5.5.5.5'], ['name' => 'mx2.example.com.', 'type' => 'MX', 'rdata' => 'mail2.example.com.', 'priority' => 15], diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsDHCPRelayTestCase.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsDHCPRelayTestCase.inc new file mode 100644 index 000000000..1f723fd7b --- /dev/null +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsDHCPRelayTestCase.inc @@ -0,0 +1,108 @@ +model_objects as $dhcp_server) { + $dhcp_server->enable->value = false; + $dhcp_server->update(); + } + if ($apply) { + (new DHCPServer(async: false))->apply(); + } + } + + /** + * Checks that the DHCPRelay cannot be enabled if any DHCPServer is already enabled. + */ + public function test_dhcp_relay_cannot_enable_if_dhcp_server_enabled(): void { + # Disable all running DHCPServers + $this->disable_dhcp_servers(); + + # Ensure we can enable the DHCPRelay + $this->assert_does_not_throw( + callable: function () { + $dhcp_relay = new DHCPRelay(enable: true, server: ['1.2.3.4']); + $dhcp_relay->update(); + }, + ); + + # Disable the DHCPRelay + $dhcp_relay = new DHCPRelay(enable: false); + $dhcp_relay->update(); + + # Enable a DHCPServer + $dhcp_server = new DHCPServer(id: 'lan', enable: true); + $dhcp_server->update(); + + # Ensure we cannot enable the DHCPRelay + $this->assert_throws_response( + response_id: 'DHCP_RELAY_CANNOT_BE_ENABLED_WHILE_DHCP_SERVER_IS_ENABLED', + code: 409, + callable: function () use ($dhcp_relay) { + $dhcp_relay = new DHCPRelay(enable: true); + $dhcp_relay->update(); + }, + ); + } + + /** + * Checks that the to_internal() method correctly appends the '_vip' prefix to the 'carpstatusvip' field. + */ + public function test_dhcp_relay_to_internal_carpstatusvip(): void { + $dhcp_relay = new DHCPRelay(carpstatusvip: 'examplecarpvip'); + $this->assert_equals($dhcp_relay->to_internal()['carpstatusvip'], '_vipexamplecarpvip'); + } + + /** + * Checks that the from_internal_carpstatusvip() method correctly removes the '_vip' prefix from the 'carpstatusvip' + * field when the value is loaded from the internal data. + */ + public function test_dhcp_relay_from_internal_carpstatusvip(): void { + $dhcp_relay = new DHCPRelay(); + $this->assert_equals($dhcp_relay->from_internal_carpstatusvip('_vipexamplecarpvip'), 'examplecarpvip'); + } + + /** + * Checks that the DHCP relay service is running after enabling and disabled after disabling the DHCPRelay. + */ + public function test_dhcp_relay_service_running(): void { + # Disable all running DHCPServers + $this->disable_dhcp_servers(apply: true); + + # Ensure the DHCPRelay is enabled + $dhcp_relay = new DHCPRelay(enable: true, interface: ['lan'], server: ['1.2.3.4']); + $dhcp_relay->update(); + + # Ensure the DHCP relay service is running with the correct arguments + $dhcrelay_process = new Command('ps aux | grep dhcrelay'); + $lan_if = $this->env['PFREST_LAN_IF']; + $wan_if = $this->env['PFREST_WAN_IF']; + $this->assert_str_contains( + $dhcrelay_process->output, + "/usr/local/sbin/dhcrelay -id $lan_if -iu $wan_if 1.2.3.4", + ); + + # Disable the DHCPRelay + $dhcp_relay = new DHCPRelay(enable: false); + $dhcp_relay->update(); + + # Ensure the DHCP relay service is not running + $dhcrelay_pid = new Command('pgrep dhcrelay'); + $this->assert_equals($dhcrelay_pid->result_code, 1); + + # Re-enable the DHCPServer on LAN + $dhcp_server = new DHCPServer(id: 'lan', enable: true); + $dhcp_server->update(apply: true); + } +} diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsDHCPServerTestCase.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsDHCPServerTestCase.inc index e2796646a..9c0600c06 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsDHCPServerTestCase.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsDHCPServerTestCase.inc @@ -34,7 +34,7 @@ class APIModelsDHCPServerTestCase extends TestCase { /** * Ensures that a DHCP server cannot be enabled on an interface is already running a DHCP relay. */ - public function test_cannot_enable_dhcp_server_with_relay_running() { + public function test_cannot_enable_dhcp_server_with_relay_running(): void { $this->assert_throws_response( response_id: 'DHCP_SERVER_CANNOT_BE_ENABLED_WITH_DHCP_RELAY', code: 400, @@ -43,7 +43,7 @@ class APIModelsDHCPServerTestCase extends TestCase { $dhcp_server = new DHCPServer(id: 'lan', async: false); # Temporarily change the `lan` interface to use dhcp IPv4 - Model::set_config('dhcrelay/0/interface', 'lan'); + Model::set_config('dhcrelay/enable', ''); # Try to enable the DHCP server $dhcp_server->validate_enable(enable: true); diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsHAProxyBackendTestCase.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsHAProxyBackendTestCase.inc index 278e84aee..ce471efd0 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsHAProxyBackendTestCase.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsHAProxyBackendTestCase.inc @@ -139,4 +139,18 @@ class APIModelsHAProxyBackendTestCase extends TestCase { $backend->delete(); $this->assert_is_false(HAProxyBackend::query(id: $backend->id)->exists()); } + + /** + * Ensure that the HAProxyBackend::$advanced_backend property is base64 encoded internally. + */ + public function test_advanced_backend_is_base64(): void { + # Ensure the advanced_backend property is base64 encoded when converted to internal config value + $backend = new HAProxyBackend(advanced_backend: 'test'); + $this->assert_equals($backend->advanced_backend->to_internal(), base64_encode('test')); + + # Ensure the advanced_backend property is base64 decoded when converted from internal config value + $backend = new HAProxyBackend(); + $backend->advanced_backend->from_internal(base64_encode('test')); + $this->assert_equals($backend->advanced_backend->value, 'test'); + } } diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsHAProxyFrontendTestCase.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsHAProxyFrontendTestCase.inc index 359862311..b656a05da 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsHAProxyFrontendTestCase.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsHAProxyFrontendTestCase.inc @@ -3,20 +3,34 @@ namespace RESTAPI\Tests; use RESTAPI\Core\TestCase; +use RESTAPI\Models\Certificate; use RESTAPI\Models\HAProxyBackend; use RESTAPI\Models\HAProxyFrontend; use RESTAPI\Models\HAProxyFrontendACL; use RESTAPI\Models\HAProxyFrontendAction; use RESTAPI\Models\HAProxyFrontendAddress; +use RESTAPI\Models\HAProxyFrontendCertificate; class APIModelsHAProxyFrontendTestCase extends TestCase { private HAProxyBackend $backend; + private Certificate $main_crt; + private Certificate $alt_crt; public array $required_packages = ['pfSense-pkg-haproxy']; /** * Setup the test case. */ public function setup(): void { + $this->main_crt = new Certificate( + descr: 'example_main_certificate', + crt: '-----BEGIN CERTIFICATE-----\nMIIEETCCAvmgAwIBAgIUFj+2UXmP+7z2RqSU1NquTnaJu3owDQYJKoZIhvcNAQEL\nBQAwgZcxCzAJBgNVBAYTAlVTMREwDwYDVQQIDAhNaWNoaWdhbjEQMA4GA1UEBwwH\nTGFuc2luZzEcMBoGA1UECgwTRXhhbXBsZSBDb21wYW55IExMQzELMAkGA1UECwwC\nSVQxFDASBgNVBAMMC2V4YW1wbGUuY29tMSIwIAYJKoZIhvcNAQkBFhNleGFtcGxl\nQGV4YW1wbGUuY29tMB4XDTI1MDEyMTE3MjU0OFoXDTM1MDEyMTE3MjU0OFowgZcx\nCzAJBgNVBAYTAlVTMREwDwYDVQQIDAhNaWNoaWdhbjEQMA4GA1UEBwwHTGFuc2lu\nZzEcMBoGA1UECgwTRXhhbXBsZSBDb21wYW55IExMQzELMAkGA1UECwwCSVQxFDAS\nBgNVBAMMC2V4YW1wbGUuY29tMSIwIAYJKoZIhvcNAQkBFhNleGFtcGxlQGV4YW1w\nbGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAt3Rc+FmQB/fk\n33faRhm9Gg9a67RHHCeEdB6OubfvSXN8/InYP6o+Ln5Wp8xmYqBtHcjnfO/iIYOH\nbl87pvKR7AiPSE1IfMSZxp8xiOhbUJwPt9QOXQA8mExW0M740+PNBQd8k+UXUb0g\n3ammsIo4NlNYvhle8l8Q2YBsK7KwsxLp6PjNLmnKOFm+BldALz1Sj3ER8XGuOfsZ\nALqxQiG87vTO2kSBSTqmorie5fgIcVVKkKhNmSfo9TWVKAr2rHBl2ep0L8o2QgeY\nuA/mPrlxc1V/JeybbENQqkiJVNz1F2120HKXGxHNv2VzWHN0rOAThG8Tmx4ej/FQ\nMRPsCKNqpwIDAQABo1MwUTAdBgNVHQ4EFgQUTlotGm0X/eXy7IpexMyOfHDtSPsw\nHwYDVR0jBBgwFoAUTlotGm0X/eXy7IpexMyOfHDtSPswDwYDVR0TAQH/BAUwAwEB\n/zANBgkqhkiG9w0BAQsFAAOCAQEAQKfpuM1jY86fd+oyfyEQJPkm4optyAdn/6FH\n+jsHejgAzwgMhLYldG8pEXR21zJZMIZuv9NqLhTJTbfbK7hZ4S3PhodYBU24xjyu\ndMzCqpW1nS8T/Hi1ULKoWQ/7fBaR1iLwA2gIvmKCWfTb0WrObxNZe3l95q1srli4\nN1XPMvmD53aBU4Yx4+pKQyPkS0tjUl2+sq/Ry1uh1Wz/2TIxn2zPfL0wnidXTwGQ\n33raRCxt+kJ0a7dmKo7ejh5dMx0o1yXeuttBiMddTJsK8GkHmX/ZJXZlSLAkJV3p\nEP/ZM3VAYeoj7iS5NC5XqXmOewiOor4ok75x2CoeoudAKENf1Q==\n-----END CERTIFICATE-----\n', + prv: '-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC3dFz4WZAH9+Tf\nd9pGGb0aD1rrtEccJ4R0Ho65t+9Jc3z8idg/qj4uflanzGZioG0dyOd87+Ihg4du\nXzum8pHsCI9ITUh8xJnGnzGI6FtQnA+31A5dADyYTFbQzvjT480FB3yT5RdRvSDd\nqaawijg2U1i+GV7yXxDZgGwrsrCzEuno+M0uaco4Wb4GV0AvPVKPcRHxca45+xkA\nurFCIbzu9M7aRIFJOqaiuJ7l+AhxVUqQqE2ZJ+j1NZUoCvascGXZ6nQvyjZCB5i4\nD+Y+uXFzVX8l7JtsQ1CqSIlU3PUXbXbQcpcbEc2/ZXNYc3Ss4BOEbxObHh6P8VAx\nE+wIo2qnAgMBAAECggEACkAZtmoGsV30BK/P2zfdSDilwLgaJbloM2FxDHEkZnW/\nUjr/gWXT7fQdh72qK4ZBjTb9sY4+aSyjY2g1zzkMLI85Nksp1YgvUW5GrcV8iK9X\nn31dRMFadrtZOcyUEal6T0wg9VXebNuaRlhlhHAMqQMU2SEAxvPHNL1HY1UA04M5\neWlwgb+WN8ocQgAWT42L/Ys3bLWilA9HfTgotLcM2AonF7MhR8kKubRzCBupaRYI\nRLD+EPJpvz7hbvLjTi8OUc2Ksl8L2ZhuIVLqYljBWWHCEj+z97i5oaUSMo8ZujVo\nMPGQkM6Blh8viR9TSVXH/o+iGnlGgABn3yESNrUUTQKBgQD0Dq1EIXtiXtTOud6O\nI4l9tieb9y9hZXCgaemr2IiH5OpsnALf5aBqNA7WhRFvBN5Jby6F5XhSLPHthQLr\nWWouyJIFUuTxGMBQE3JFOTxn+mikH1nsiuZTqYvZtbfSyiuJA2qMpvegowX+Y0gX\n3HCIUBmNOZW44GsQyYxlzBZeDQKBgQDAboLRGnDTZF+Kg+XcrmTOchW3vrzhIaN+\n25wLxYBBuL8po0XchA2lR9DYl/z+HLl24Qv3x/kWDfqw3YMjeSuJDCiRW6k2nyOs\nBq9iIWJyG120retNeil/ee+Oi4Ot6heMBLhsiPokNiyQErFjFVDja0NWNI3RLT3O\nXl0NnC3ygwKBgQDPIWmyCgySl1NbQ+p7jfORqrgBKY+fxJ0Kt/jmrPZrB0Bh/yJ9\nbYBwwk/ZhsQeCe6yOlcssm0kqRqGcD3jClcQ747mT80YRd5p16dC0unOgXqv2B/U\nnqoOVTXpBs3Fa2n5ddnHF6nQPqvGk/JKtf8X4YteDYJhjq2vaK7scoKFPQKBgQCK\nz20IP6WBp9cPiUPH4/kNvtgRusJvOB/DQ9GV4Ds7rXQgKgocHxnkzyBFDaZO/BOB\nQKesbpEsqpra+H8/mImiC6Y77L3si/Fkh1H+XIyyOTFoQ0kIk4XZ0fDPh65ORx4D\nP+h1Sn3+nyGYMBPLoLW/x0FErxZZ/OOnuQQw7Cds1wJ/BRP+3rVtm8hof1hk7gRS\nUlhuViYUScnBfuuCGp5Fdj1tfuMdqDA0IB7u+7V4sIy4z8WBmeZFsjE67eMTv9uR\nC6tjrdA8DUjiUnSZTwBWzjPZ8u87BtD4y3TZqdhA/a2yd1u6L9Uw7g/JzDpAiMIG\n5Wj8TdEgyCArcswKTBRvZg==\n-----END PRIVATE KEY-----\n', + ); + $this->alt_crt = new Certificate( + descr: 'example_alt_certificate', + crt: '-----BEGIN CERTIFICATE-----\nMIIEITCCAwmgAwIBAgIUEuRnc2GSFACHYusB06+IAjZ/XZ0wDQYJKoZIhvcNAQEL\nBQAwgZ8xCzAJBgNVBAYTAlVTMREwDwYDVQQIDAhJbGxpbm9pczEQMA4GA1UEBwwH\nQ2hpY2FnbzEcMBoGA1UECgwTRXhhbXBsZSBDb21wYW55IExMQzELMAkGA1UECwwC\nSVQxGDAWBgNVBAMMD2FsdC5leGFtcGxlLmNvbTEmMCQGCSqGSIb3DQEJARYXZXhh\nbXBsZUBhbHQuZXhhbXBsZS5jb20wHhcNMjUwMTIxMTcyODQxWhcNMzUwMTIxMTcy\nODQxWjCBnzELMAkGA1UEBhMCVVMxETAPBgNVBAgMCElsbGlub2lzMRAwDgYDVQQH\nDAdDaGljYWdvMRwwGgYDVQQKDBNFeGFtcGxlIENvbXBhbnkgTExDMQswCQYDVQQL\nDAJJVDEYMBYGA1UEAwwPYWx0LmV4YW1wbGUuY29tMSYwJAYJKoZIhvcNAQkBFhdl\neGFtcGxlQGFsdC5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC\nAQoCggEBAK/xDL+IK455MhTv86XU/PSAzrrPNbD1f03KxybPr/xX2AFTnIYkez53\nHWMnpizn5e1HoOqXHynptE/wGLsgE105H7aGZ8t52jbqifIIUhTi+HBRC2Yg4YRf\n6PbR+1lvMaCofotz4zMtZtpp/XiOXrSHqulGcV+bSgtu2uxu9wIfwxN2Mvg/8sk7\noqNimwLpuRU0ZqAYt6dFsxsvCZC8gCywYL45ML2KxociWAjFDp0obj7g/j5XSxEj\nAz5KfZdJhxl0NSCD+ewWUYYsnCRWHbojtqlQnGY4QXTYZ6TLGyOB5jPYK7cRCMXb\npsveo24c29UBStiJ1u87gu+PGqUhpVsCAwEAAaNTMFEwHQYDVR0OBBYEFGhql+aY\nSxnUlNLett0Oqb+O6OcqMB8GA1UdIwQYMBaAFGhql+aYSxnUlNLett0Oqb+O6Ocq\nMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAKQh880ZOtQeW2VH\nMqILfcF/qf/Ge5kCmS/8mTAAZinvSuCISblcBIMgei3L7LObav48u2BmNFor1o1o\naB4iSMLRyEGNrnYhCy4IrCWw7Y2YjzYJz5oEUy9jJ9S4zOZGV/qNVIhiBSJN274Z\nU8Q8kmF/Tpze1G65AipUgbaw3fx8VgvlOotemtDNsIwMA9WMDnXMqVOBIs5yWWp9\nT9f6Y8to/UcNm0JS7LExn9xTi5WAxstUzk5tWsvJ5pzEppUymSdkSpzObRcqRLZa\n5YeFbd+svKfAue8iukzwuukDsHl8lmwkSASFRwdh3391JC7lpxenAFjs0I1eXUBs\nBz371as=\n-----END CERTIFICATE-----\n', + prv: '-----BEGIN PRIVATE KEY-----\nMIIEuwIBADANBgkqhkiG9w0BAQEFAASCBKUwggShAgEAAoIBAQCv8Qy/iCuOeTIU\n7/Ol1Pz0gM66zzWw9X9Nyscmz6/8V9gBU5yGJHs+dx1jJ6Ys5+XtR6Dqlx8p6bRP\n8Bi7IBNdOR+2hmfLedo26onyCFIU4vhwUQtmIOGEX+j20ftZbzGgqH6Lc+MzLWba\naf14jl60h6rpRnFfm0oLbtrsbvcCH8MTdjL4P/LJO6KjYpsC6bkVNGagGLenRbMb\nLwmQvIAssGC+OTC9isaHIlgIxQ6dKG4+4P4+V0sRIwM+Sn2XSYcZdDUgg/nsFlGG\nLJwkVh26I7apUJxmOEF02GekyxsjgeYz2Cu3EQjF26bL3qNuHNvVAUrYidbvO4Lv\njxqlIaVbAgMBAAECgf9u/iKMsdrIhqyRiM6sTzFFDf1c3FuCx10INmRsl5juGHys\nWZLlDR6yyXJAm0K6EZF7nzRkyfFe/5BA3ba8Vf/hT6gx/Zh9ROHkwxFDHvypMIaK\nVJZcV2HtoJPXIaDvSraEI8exMeqi3oGESFkfNLGKMgwgHdBoAA82Jzj07wvkyhjG\nuB2KZ0x8bDOuan9nM8EfJ6ncN8/biOolPfOv/ojUDjp0yN//gkPQTij50Dj8dxEU\nwobi9AE3diszC8gjj5i4mX14qWKC6/xxGcSHoxsPYUJt7nYGb5RRZciqChroob0o\nDP8VpQR/SnqY8OU97xExbjS0zjvlhipPgiMbGKECgYEA3SVdbR1niZrNEly2S9y1\nsL+pzl2t6KA6pd6XFvlMqgqAtCo9MOVDXkqfptKt9oT+MC8SkbwFFIYLWXHMBNcO\nqfVCIK1mp5scHRgiBdmjo0yEXIsJ5J1y+bm8mTlyGJHbefh7FQoHnrJ/7X9WXuQo\nZWetv196PQuOiLUL49NukGECgYEAy6vQBznsessCeetXOHOO7ap9dieoWzFK0YIX\nZaA9jX+REgIhhIxgWkBcsMQAlm5B74YskYcbsrUm510xn0stzzh91O6DyoM3PJJc\nrqeUJU+HEYELcuZCVy4RV2+XMjSLPc5Sivgbe0mOfHKz3szNIwGKZGuRrWIT6djA\nUyuuvzsCgYA7Qdzj9SIeTD2xMuiiMVT6NJFyu8Vy9SRh9+AyhsDVO5U7MapN5NEF\nfieDkyaMTRyzmpl2NZSC6Fw4LncFwP6r6g07JlvAg56n6SSMvsHLayB4j8Up3krF\nwdBYmn0JOmQ+LagFfjeGfbwrzpg9OxPcAEkdY58kpmkeFq1F8gzJYQKBgQCrb0d/\njaYcBd1julCEV+Cq9KL+XYs8l1Ue3J3NEQA+pm359ok4BODllt9ueszz0oL1lr7V\nECCkYA8LGEq4hpNcpXRlUw7j22cXOSuCZ9QvQ62xcims4Vxd/YzjSC7AN7IsqAqa\npxSaxeOFpRb6JvFU0esnc9P/WVrhr2zTlSVWGQKBgCFaRF3yP/Tc8EvYEhCPgRLv\nsM5FZUe/86gWoWUhSoXcPpW+CipjatcB3kLxXXcYMFYl4cVe5KwTemMmDgMMiMdP\nwn+R2SGvU89GTCTWmlUeD4sesNVFcHMe2Rw0pBf1BTh5KIsA9BXRe13bWHU+x8T2\nSIeb+boJuWOH4flIIBud\n-----END PRIVATE KEY-----\n', + ); $this->backend = new HAProxyBackend( name: 'example_backend', balance: '', @@ -72,6 +86,8 @@ class APIModelsHAProxyFrontendTestCase extends TestCase { transparent_interface: 'lan', ); $this->backend->create(); + $this->main_crt->create(); + $this->alt_crt->create(); } /** @@ -79,6 +95,8 @@ class APIModelsHAProxyFrontendTestCase extends TestCase { */ public function teardown(): void { $this->backend->delete(); + $this->main_crt->delete(); + $this->alt_crt->delete(); } /** @@ -122,6 +140,12 @@ class APIModelsHAProxyFrontendTestCase extends TestCase { forwardfor: false, httpclose: 'http-keep-alive', advanced: '', + ssloffloadcert: $this->main_crt->refid->value, + ha_certificates: [ + [ + 'ssl_certificate' => $this->alt_crt->refid->value, + ], + ], ); $frontend->create(); @@ -135,6 +159,7 @@ class APIModelsHAProxyFrontendTestCase extends TestCase { $this->assert_equals($read_frontend->backend_serverpool->value, 'example_backend'); $this->assert_equals($read_frontend->client_timeout->value, 30000); $this->assert_equals($read_frontend->httpclose->value, 'http-keep-alive'); + $this->assert_equals($read_frontend->ssloffloadcert->value, $this->main_crt->refid->value); # Ensure the nested HAProxyBackend object linked up with the foreign object correctly $this->assert_is_true($read_frontend->backend_serverpool->get_related_model() instanceof HAProxyBackend); @@ -164,6 +189,14 @@ class APIModelsHAProxyFrontendTestCase extends TestCase { $this->assert_equals($read_frontend->a_actionitems->value[0]['acl'], 'mail_acl'); $this->assert_equals($read_frontend->a_actionitems->value[0]['lua_function'], 'example function'); + # Ensure the nested HAProxyFronendCertificate object was created successful + $this->assert_is_not_empty($read_frontend->ha_certificates->value); + $this->assert_is_true(HAProxyFrontendCertificate::query(parent_id: $frontend->id, id: 0)->exists()); + $this->assert_equals( + $read_frontend->ha_certificates->value[0]['ssl_certificate'], + $this->alt_crt->refid->value, + ); + # Update the object and ensure the changes were successful $frontend->status->value = 'disabled'; $frontend->update(); diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsServiceTestCase.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsServiceTestCase.inc index c4483fb60..fb62b1778 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsServiceTestCase.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsServiceTestCase.inc @@ -22,37 +22,12 @@ class APIModelsServiceTestCase extends TestCase { } } - /** - * Checks that the `get_service_name_choices()` method correctly identifies all available service names. - */ - public function test_get_service_name_choices(): void { - # Ensure expected services are found in the method's response - $expected_services = ['unbound', 'ntpd', 'syslogd', 'dhcpd', 'dpinger', 'sshd']; - - # Loop through each expected service and ensure it is found - foreach ($expected_services as $service) { - $this->assert_is_true(in_array($service, (new Service())->get_service_name_choices())); - } - } - - /** - * Checks that the `get_id_by_name()` correctly obtains the ID of the Service object with a specific `name` - */ - public function test_get_id_by_name(): void { - # Create a Service object to test with - $test_service = new Service(); - - # Obtain the ID of the `sshd` service and ensure the correct ID was given - $sshd_id = $test_service->get_id_by_name('sshd'); - $this->assert_equals($sshd_id, $test_service->query(['name' => 'sshd'])->first()->id); - } - /** * Checks that a Service can be stopped, started and restarted. */ public function test_can_perform_service_actions(): void { - # Define a Service to test with - $test_service = new Service(data: ['name' => 'ntpd']); + # Obtain a Service to test with + $test_service = Service::query(name: 'ntpd')->first(); # Ensure the service can be stopped $test_service->action->value = 'stop'; diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsWireGuardSettingsTestCase.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsWireGuardSettingsTestCase.inc index 4dde87a45..aa1912c51 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsWireGuardSettingsTestCase.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsWireGuardSettingsTestCase.inc @@ -19,12 +19,12 @@ class APIModelsWireGuardSettingsTestCase extends TestCase { # Enable WireGuard and ensure the service is running $wg = new WireGuardSettings(enable: true, async: false); $wg->update(apply: true); - $this->assert_is_true(Service::query(name: 'wireguard', status: true)->exists()); + $this->assert_is_true(is_service_running('wireguard')); # Disable WireGuard and ensure the service is stopped $wg->enable->value = false; $wg->update(apply: true); - $this->assert_is_true(Service::query(name: 'wireguard', status: false)->exists()); + $this->assert_is_false(is_service_running('wireguard')); } /**