Skip to content

Commit f172175

Browse files
fix(IPsecPhase2Encryption): allow 'auto' keylen to be represented as 0 #880
1 parent c89de07 commit f172175

4 files changed

Lines changed: 335 additions & 44 deletions

File tree

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
<?php
2+
3+
namespace RESTAPI\Fields;
4+
5+
require_once 'RESTAPI/autoloader.inc';
6+
7+
use RESTAPI;
8+
use RESTAPI\Core\Field;
9+
10+
/**
11+
* Defines a Field object for validating and storing key length values in the pfSense configuration. This is namely
12+
* for IPsec Phase 2 entries to handle the 'auto' keyword as an integer.
13+
*/
14+
class KeyLenField extends Field {
15+
/**
16+
* Defines the KeyLenField object and sets its options.
17+
* @param bool $required If `true`, this field is required to have a value at all times.
18+
* @param bool $unique If `true`, this field must be unique from all other parent model objects. Enabling this
19+
* option requires the Model $context to be set AND the Model $context must have a `config_path` set.
20+
* @param mixed|null $default Assign a default string value to assign this Field if no value is present.
21+
* @param string $default_callable Defines a callable method that should be called to populate the default value
22+
* for this field. It is strongly encouraged to use a default callable when the default is variable and may change
23+
* dynamically.
24+
* @param array $choices An array of value choices this Field can be assigned. This can either be an indexed array
25+
* of the exact choice values, or an associative array where the array key is the exact choice value and the array
26+
* value is a verbose name for the choice. Verbose choice name are used by ModelForms when generating web pages
27+
* for a given Model.
28+
* @param string $choices_callable Assign a callable method from this Field object OR the parent Model context to
29+
* execute to populate choices for this field. This callable must be a method assigned on this Field object OR the
30+
* parent Model object that returns an array of valid choices in the same format as $choices. This is helpful when
31+
* choices are dynamic and must be populate at runtime instead of pre-determined sets of values.
32+
* @param bool $allow_null If `true`, null values will be allowed by this field.
33+
* @param bool $editable Set to `false` to prevent this field's value from being changed after its initial creation.
34+
* @param bool $read_only If `true`, this field can only read its value and cannot write its value to config.
35+
* @param bool $write_only Set to `true` to make this field write-only. This will prevent the field's current value
36+
* from being displayed in the representation data. This is ideal for potentially sensitive Fields like passwords,
37+
* keys, and hashes.
38+
* @param bool $representation_only Set to `true` to make this field only present in its representation form. This
39+
* effectively prevents the Field from being converted to an internal value which is saved to the pfSense config.
40+
* This should only be used for Fields that do not relate directly to a configuration value.
41+
* @param bool $many If `true`, the value must be an array of many strings.
42+
* @param int $many_minimum When $many is set to `true`, this sets the minimum number of array entries required.
43+
* @param int $many_maximum When $many is set to `true`, this sets the maximum number of array entries allowed.
44+
* @param int $minimum The minimum value this value can be.
45+
* @param int $maximum The maximum value this value can be.
46+
* @param string|null $delimiter Assigns the string delimiter to use when writing array values to config.
47+
* Use `null` if this field is stored as an actual array in config. This is only available if $many is set to
48+
* `true`. Defaults to `,` to store as comma-separated string.
49+
* @param string $verbose_name The detailed name for this Field. This name will be used in non-programmatic areas
50+
* like web pages and help text. This Field will default to property name assigned to the parent Model with
51+
* underscores converted to spaces.
52+
* @param string $verbose_name_plural The plural form of $verbose_name. This defaults to $verbose_name with `s`
53+
* suffixed or `es` suffixes to strings already ending with `s`.
54+
* @param string $internal_name Assign a different field name to use when referring to the internal field as it's
55+
* stored in the pfSense configuration.
56+
* @param string $internal_namespace Sets the namespace this field belongs to internally. This can be used to nest
57+
* the Fields internal value under a specific namespace as an associative array. This only applies to the internal
58+
* value, not the representation value.
59+
* @param array $referenced_by An array that specifies other Models and Field's that reference this Field's parent
60+
* Model using this Field's value. This will prevent the parent Model object from being deleted while it is actively
61+
* referenced by another Model object. The array key must be the name of the Model class that references this Field,
62+
* and the value must be a Field within that Model. The framework will automatically search for any existing Model
63+
* objects that have the referenced Field assigned a value that matches this Field's value.
64+
* @param array $conditions An array of conditions the field must meet to be included. This allows you to specify
65+
* conditions of other Fields within the parent Model context. For example, if the parent Model context has two
66+
* Fields, one field named `type` and the other being this field; and you only want this field to be included if
67+
* `type` is equal to `type1`, you could assign ["type" => "type1"] to this parameter.
68+
* @param array $validators An array of Validator objects to run against this field.
69+
* @param string $help_text Set a description for this field. This description will be used in API documentation.
70+
*/
71+
public function __construct(
72+
bool $required = false,
73+
bool $unique = false,
74+
mixed $default = null,
75+
string $default_callable = '',
76+
array $choices = [],
77+
string $choices_callable = '',
78+
bool $allow_null = false,
79+
bool $editable = true,
80+
bool $read_only = false,
81+
bool $write_only = false,
82+
bool $representation_only = false,
83+
bool $many = false,
84+
int $many_minimum = 0,
85+
int $many_maximum = Field::MANY_MAXIMUM,
86+
public int $minimum = 0,
87+
public int $maximum = PHP_INT_MAX,
88+
string|null $delimiter = ',',
89+
string $verbose_name = '',
90+
string $verbose_name_plural = '',
91+
string $internal_name = '',
92+
string $internal_namespace = '',
93+
array $referenced_by = [],
94+
array $conditions = [],
95+
array $validators = [],
96+
string $help_text = '',
97+
) {
98+
parent::__construct(
99+
type: 'integer',
100+
required: $required,
101+
unique: $unique,
102+
default: $default,
103+
default_callable: $default_callable,
104+
choices: $choices,
105+
choices_callable: $choices_callable,
106+
allow_null: $allow_null,
107+
editable: $editable,
108+
read_only: $read_only,
109+
write_only: $write_only,
110+
representation_only: $representation_only,
111+
many: $many,
112+
many_minimum: $many_minimum,
113+
many_maximum: $many_maximum,
114+
delimiter: $delimiter,
115+
verbose_name: $verbose_name,
116+
verbose_name_plural: $verbose_name_plural,
117+
internal_name: $internal_name,
118+
internal_namespace: $internal_namespace,
119+
referenced_by: $referenced_by,
120+
conditions: $conditions,
121+
validators: $validators + [
122+
new RESTAPI\Validators\NumericRangeValidator(minimum: $minimum, maximum: $maximum),
123+
],
124+
help_text: $help_text,
125+
);
126+
}
127+
128+
/**
129+
* Converts the field value from its representation value into it's internal value. This namely handles converting
130+
* 0 to 'auto'.
131+
* @param mixed $representation_value The representation value to convert to its internal value
132+
*/
133+
protected function _to_internal(mixed $representation_value): array|string|null {
134+
if ($representation_value === 0) {
135+
return parent::_to_internal('auto');
136+
}
137+
return parent::_to_internal($representation_value);
138+
}
139+
140+
/**
141+
* Converts the field value to its representation form from its internal pfSense configuration value.
142+
* @param string $internal_value The internal value from the pfSense configuration.
143+
* @return int The field value in its representation form.
144+
*/
145+
protected function _from_internal(mixed $internal_value): mixed {
146+
# Return the value as an integer if it's numeric
147+
if (is_numeric($internal_value)) {
148+
return intval($internal_value);
149+
}
150+
151+
# If the value is 'auto', return 0 (0 is the representation we use for auto)
152+
if ($internal_value === 'auto') {
153+
return 0;
154+
}
155+
156+
# If the value is an empty string, assume it's null
157+
if ($internal_value === '') {
158+
return null;
159+
}
160+
161+
# Otherwise, the internal value cannot be represented by this Field. Throw an error.
162+
throw new RESTAPI\Responses\ServerError(
163+
message: "Cannot parse KeyLenField '$this->name' from internal because its internal value is not a " .
164+
"numeric value or 'auto'. Consider changing this field to a StringField.",
165+
response_id: 'KEYLEN_FIELD_WITH_NON_INTEGER_INTERNAL_VALUE',
166+
);
167+
}
168+
169+
/**
170+
* Converts this Field object to a PHP array representation of an OpenAPI schema property configuration. This is
171+
* used when auto-generating API documentation. This method can be extended to add additional options to the OpenAPI
172+
* schema property.
173+
* @link https://swagger.io/docs/specification/data-models/
174+
* @return array A PHP array containing this field as a OpenAPI schema property configuration.
175+
*/
176+
public function to_openapi_property(): array {
177+
# Run the parent to_openapi_property() to obtain the base property object, then make changes as needed.
178+
$openapi_property = parent::to_openapi_property();
179+
180+
# Add the minimum and maximum to the OpenAPI property.
181+
if ($this->many) {
182+
$openapi_property['items']['minimum'] = $this->minimum;
183+
$openapi_property['items']['maximum'] = $this->maximum;
184+
} else {
185+
$openapi_property['minimum'] = $this->minimum;
186+
$openapi_property['maximum'] = $this->maximum;
187+
}
188+
189+
return $openapi_property;
190+
}
191+
192+
/**
193+
* Converts this Field object into a pfSense webConfigurator form input. This method can be overridden by a child
194+
* class to add custom input field creation.
195+
* @param string $type The HTML input tag type. Not all Fields support input types.
196+
* @param array $attributes An array of additional HTML input tag attributes. Not all Fields support input attributes.
197+
* @return object The pfSense webConfigurator form input object.
198+
* @link https://github.com/pfsense/pfsense/tree/master/src/usr/local/www/classes/Form
199+
*/
200+
public function to_form_input(string $type = 'number', array $attributes = []): object {
201+
$attributes += ['min' => $this->minimum, 'max' => $this->maximum];
202+
return parent::to_form_input(type: $type, attributes: $attributes);
203+
}
204+
}

pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/IPsecPhase2Encryption.inc

Lines changed: 8 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ namespace RESTAPI\Models;
55
use RESTAPI\Core\Model;
66
use RESTAPI\Dispatchers\IPsecApplyDispatcher;
77
use RESTAPI\Fields\IntegerField;
8+
use RESTAPI\Fields\KeyLenField;
89
use RESTAPI\Fields\StringField;
910
use RESTAPI\Responses\ValidationError;
1011

@@ -13,7 +14,7 @@ use RESTAPI\Responses\ValidationError;
1314
*/
1415
class IPsecPhase2Encryption extends Model {
1516
public StringField $name;
16-
public IntegerField $keylen;
17+
public KeyLenField $keylen;
1718

1819
public function __construct(mixed $id = null, mixed $parent_id = null, array $data = [], mixed ...$options) {
1920
# Obtain global p2 algorithm variables
@@ -34,38 +35,17 @@ class IPsecPhase2Encryption extends Model {
3435
internal_name: 'name',
3536
help_text: 'The name of the encryption algorithm to use for this P2 encryption item.',
3637
);
37-
$this->keylen = new IntegerField(
38+
$this->keylen = new KeyLenField(
3839
required: true,
40+
choices_callable: 'get_supported_keylens',
3941
internal_name: 'keylen',
4042
conditions: ['name' => $this->get_keylen_enabled_algos()],
41-
help_text: 'The key length for the encryption algorithm.',
43+
help_text: 'The key length for the encryption algorithm. Use 0 to select key length automatically.',
4244
);
4345

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

47-
/**
48-
* Adds extra validation to the `keylen` field.
49-
* @param int $keylen The incoming value to be validated.
50-
* @returns int The validated value to be assigned.
51-
* @throws ValidationError When the $keylen is not supported by the
52-
* `name` field's assigned value.
53-
*/
54-
public function validate_keylen(int $keylen): int {
55-
# Variables
56-
$supported_keylens = $this->get_supported_keylens(name: $this->name->value);
57-
58-
# Throw a validation error if this $keylen is not supported for the assigned algo
59-
if (!in_array($keylen, $supported_keylens)) {
60-
throw new ValidationError(
61-
message: "Field `keylen` value `$keylen` is not valid for the `{$this->name->value}` algorithm.",
62-
response_id: 'IPSEC_PHASE_2_ENCRYPTION_ALGORITHM_KEYLEN_INVALID_CHOICE',
63-
);
64-
}
65-
66-
return $keylen;
67-
}
68-
6949
/**
7050
* Obtains all supported key lengths for an encryption algorithm with a provided algorithm name.
7151
* @param string $name The encryption algorithm name to obtain key lengths for.
@@ -100,6 +80,9 @@ class IPsecPhase2Encryption extends Model {
10080
}
10181
}
10282

83+
# Accept '0', as this is the representation keyword for 'auto'
84+
$key_lens[] = 0;
85+
10386
return $key_lens;
10487
}
10588

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
<?php
2+
3+
namespace RESTAPI\Tests;
4+
5+
use RESTAPI\Core\TestCase;
6+
use RESTAPI\Fields\KeyLenField;
7+
8+
class APIFieldsKeyLenFieldTestCase extends TestCase {
9+
/**
10+
* Checks that KeyLenField always includes a NumericRangeValidator.
11+
*/
12+
public function test_key_len_field_has_numeric_range_validator(): void {
13+
$field = new KeyLenField();
14+
$this->assert_equals(get_class($field->validators[0]), 'RESTAPI\Validators\NumericRangeValidator');
15+
}
16+
17+
/**
18+
* Checks that `_from_internal()` converts a numeric string to an integer.
19+
*/
20+
public function test_from_internal_numeric_string_returns_integer(): void {
21+
$field = new KeyLenField();
22+
$field->from_internal('256');
23+
$this->assert_equals($field->value, 256);
24+
$this->assert_is_true(is_int($field->value));
25+
}
26+
27+
/**
28+
* Checks that `_from_internal()` converts the 'auto' keyword to 0.
29+
*/
30+
public function test_from_internal_auto_returns_zero(): void {
31+
$field = new KeyLenField();
32+
$field->from_internal('auto');
33+
$this->assert_equals($field->value, 0);
34+
}
35+
36+
/**
37+
* Checks that `_from_internal()` converts an empty string to null.
38+
*/
39+
public function test_from_internal_empty_string_returns_null(): void {
40+
$field = new KeyLenField(allow_null: true);
41+
$field->from_internal('');
42+
$this->assert_is_null($field->value);
43+
}
44+
45+
/**
46+
* Checks that `_from_internal()` throws a ServerError for non-numeric, non-'auto' values.
47+
*/
48+
public function test_from_internal_invalid_value_throws_server_error(): void {
49+
$this->assert_throws_response(
50+
response_id: 'KEYLEN_FIELD_WITH_NON_INTEGER_INTERNAL_VALUE',
51+
code: 500,
52+
callable: function () {
53+
$field = new KeyLenField();
54+
$field->from_internal('not_valid');
55+
},
56+
);
57+
}
58+
59+
/**
60+
* Checks that `keylen` can be assigned the value 0 and that `to_internal()` converts it to 'auto'.
61+
*/
62+
public function test_to_internal_zero_returns_auto(): void {
63+
$field = new KeyLenField(default: 0);
64+
$field->value = 0;
65+
$this->assert_equals($field->value, 0);
66+
$this->assert_equals($field->to_internal(), 'auto');
67+
}
68+
69+
/**
70+
* Checks that `to_internal()` passes non-zero integers through normally.
71+
*/
72+
public function test_to_internal_non_zero_passes_through(): void {
73+
$field = new KeyLenField(default: 128);
74+
$field->value = 128;
75+
$this->assert_equals($field->to_internal(), 128);
76+
}
77+
78+
/**
79+
* Checks that `to_openapi_property()` includes `minimum` and `maximum` for non-many fields.
80+
*/
81+
public function test_to_openapi_property_includes_min_max(): void {
82+
$field = new KeyLenField(minimum: 64, maximum: 512);
83+
$property = $field->to_openapi_property();
84+
$this->assert_equals($property['minimum'], 64);
85+
$this->assert_equals($property['maximum'], 512);
86+
}
87+
88+
/**
89+
* Checks that `to_openapi_property()` includes `minimum` and `maximum` under `items` for many fields.
90+
*/
91+
public function test_to_openapi_property_includes_min_max_for_many(): void {
92+
$field = new KeyLenField(many: true, minimum: 64, maximum: 512);
93+
$property = $field->to_openapi_property();
94+
$this->assert_equals($property['items']['minimum'], 64);
95+
$this->assert_equals($property['items']['maximum'], 512);
96+
}
97+
}

0 commit comments

Comments
 (0)