Skip to content

Commit 1173718

Browse files
feat(FreeRADIUSMAC): add models, endpoints and tests for /api/v2/services/freeradius/mac #21
1 parent 11a9e38 commit 1173718

6 files changed

Lines changed: 649 additions & 251 deletions

File tree

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
namespace RESTAPI\Endpoints;
4+
5+
require_once 'RESTAPI/autoloader.inc';
6+
7+
use RESTAPI\Core\Endpoint;
8+
9+
/**
10+
* Defines an Endpoint for interacting with a single FreeRADIUSMAC Model object at
11+
* /api/v2/services/freeradius/mac
12+
*/
13+
class ServicesFreeRADIUSMACEndpoint extends Endpoint {
14+
public function __construct() {
15+
/**
16+
* Set Endpoint attributes
17+
*/
18+
$this->url = '/api/v2/services/freeradius/mac';
19+
$this->model_name = 'FreeRADIUSMAC';
20+
$this->many = false;
21+
$this->request_method_options = ['GET', 'POST', 'PATCH', 'DELETE'];
22+
23+
# Construct the parent Endpoint object
24+
parent::__construct();
25+
}
26+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
namespace RESTAPI\Endpoints;
4+
5+
require_once 'RESTAPI/autoloader.inc';
6+
7+
use RESTAPI\Core\Endpoint;
8+
9+
/**
10+
* Defines an Endpoint for interacting with many FreeRADIUSMAC Model objects at
11+
* /api/v2/services/freeradius/macs
12+
*/
13+
class ServicesFreeRADIUSMACsEndpoint extends Endpoint {
14+
public function __construct() {
15+
/**
16+
* Set Endpoint attributes
17+
*/
18+
$this->url = '/api/v2/services/freeradius/macs';
19+
$this->model_name = 'FreeRADIUSMAC';
20+
$this->many = true;
21+
$this->request_method_options = ['GET', 'PUT', 'DELETE'];
22+
23+
# Construct the parent Endpoint object
24+
parent::__construct();
25+
}
26+
}
Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
<?php
2+
3+
namespace RESTAPI\ModelTraits;
4+
5+
require_once 'RESTAPI/autoloader.inc';
6+
7+
use RESTAPI\Fields\IntegerField;
8+
use RESTAPI\Fields\StringField;
9+
use RESTAPI\Validators\IPAddressValidator;
10+
use RESTAPI\Validators\RegexValidator;
11+
use RESTAPI\Validators\URLValidator;
12+
13+
/**
14+
* Defines a set of Field declarations and helpers that are shared between the FreeRADIUS user-style Models
15+
* (FreeRADIUSUser, FreeRADIUSMAC). Both pfSense FreeRADIUS user and authorized-MAC entries expose the same
16+
* set of RADIUS attribute fields (description, framed addressing, VLAN, time/quota, bandwidth, additional
17+
* RADIUS attributes, etc.) — the only difference is the internal pfSense config key prefix
18+
* (`varusers...` vs `varmacs...`) and a small naming quirk on the WISPr redirection URL field.
19+
*/
20+
trait FreeRADIUSCommonModelTraits {
21+
public StringField $description;
22+
public StringField $framed_ip_address;
23+
public StringField $framed_ip_netmask;
24+
public StringField $framed_route;
25+
public StringField $framed_ipv6_address;
26+
public StringField $framed_ipv6_route;
27+
public StringField $vlan_id;
28+
public StringField $wispr_redirection_url;
29+
public IntegerField $simultaneous_connect;
30+
public StringField $expiration;
31+
public IntegerField $session_timeout;
32+
public StringField $login_time;
33+
public IntegerField $amount_of_time;
34+
public StringField $point_of_time;
35+
public IntegerField $max_total_octets;
36+
public StringField $max_total_octets_time_range;
37+
public IntegerField $max_bandwidth_down;
38+
public IntegerField $max_bandwidth_up;
39+
public IntegerField $acct_interim_interval;
40+
public StringField $top_additional_options;
41+
public StringField $check_items_additional_options;
42+
public StringField $reply_items_additional_options;
43+
44+
/**
45+
* Defines the set of Fields that are shared between the FreeRADIUSUser and FreeRADIUSMAC models.
46+
*
47+
* @param string $prefix The internal pfSense config key prefix used by the consuming Model
48+
* (e.g. `varusers` for users or `varmacs` for MAC entries).
49+
* @param string $url_field_suffix The suffix appended to $prefix to compute the internal_name
50+
* for the WISPr redirection URL field. The pfSense package uses `wisprredirectionurl` for
51+
* users and the (typo'd) `swisprredirectionurl` for MACs, so this is overridable.
52+
*/
53+
protected function init_freeradius_common_fields(
54+
string $prefix,
55+
string $url_field_suffix = 'wisprredirectionurl',
56+
): void {
57+
$this->description = new StringField(
58+
required: false,
59+
default: '',
60+
allow_empty: true,
61+
verbose_name: 'Description',
62+
validators: [
63+
new RegexValidator(
64+
pattern: '/^[a-zA-Z0-9 _,.;:+=()-]*$/',
65+
error_msg: 'Value contains invalid characters.',
66+
),
67+
],
68+
help_text: 'A description for this entry.',
69+
);
70+
$this->framed_ip_address = new StringField(
71+
required: false,
72+
default: '',
73+
allow_empty: true,
74+
verbose_name: 'Framed IP Address',
75+
internal_name: "{$prefix}framedipaddress",
76+
validators: [new IPAddressValidator(allow_ipv4: true, allow_ipv6: false)],
77+
help_text: 'Framed-IP-Address MUST be supported by NAS. ' .
78+
'If the OpenVPN server uses a subnet style Topology the RADIUS server MUST ' .
79+
'also send back an appropriate Framed-IP-Netmask value matching the VPN Tunnel Network.',
80+
);
81+
$this->framed_ip_netmask = new StringField(
82+
required: false,
83+
default: '',
84+
allow_empty: true,
85+
verbose_name: 'Framed IP Netmask',
86+
internal_name: "{$prefix}framedipnetmask",
87+
validators: [new IPAddressValidator(allow_ipv4: true, allow_ipv6: false)],
88+
help_text: 'Framed-IP-Netmask MUST be supported by NAS.',
89+
);
90+
$this->framed_route = new StringField(
91+
required: false,
92+
default: '',
93+
allow_empty: true,
94+
verbose_name: 'IPv4 Gateway',
95+
internal_name: "{$prefix}framedroute",
96+
help_text: 'Framed-Route must be supported by NAS. Required format: ' .
97+
'Subnet Gateway Metric(s) (e.g. 192.168.10.0/24 192.168.10.1 1).',
98+
);
99+
$this->framed_ipv6_address = new StringField(
100+
required: false,
101+
default: '',
102+
allow_empty: true,
103+
verbose_name: 'IPv6 Address',
104+
internal_name: "{$prefix}framedip6address",
105+
help_text: 'When the IPv6 prefix part is empty it uses Framed-IPv6-Address. ' .
106+
'When the prefix part is filled in, it uses Framed-IPv6-Prefix. ' .
107+
'Example: 2001:db8:abab::5 or 2001:db8:abab::/64',
108+
);
109+
$this->framed_ipv6_route = new StringField(
110+
required: false,
111+
default: '',
112+
allow_empty: true,
113+
verbose_name: 'IPv6 Gateway',
114+
internal_name: "{$prefix}framedip6route",
115+
help_text: 'Framed-IPv6-Route must be supported by NAS. Required format: ' .
116+
'Prefix Gateway Metric(s) (e.g. 2001:db8:0:16::/64 2001:db8::16:a0:20ff:fe99:a998 1).',
117+
);
118+
$this->vlan_id = new StringField(
119+
required: false,
120+
default: '',
121+
allow_empty: true,
122+
verbose_name: 'VLAN ID',
123+
internal_name: "{$prefix}vlanid",
124+
help_text: 'The VLAN ID (integer from 1-4095) or the VLAN name that this entry should be assigned to. ' .
125+
'Must be supported by the NAS.',
126+
);
127+
$this->wispr_redirection_url = new StringField(
128+
required: false,
129+
default: '',
130+
allow_empty: true,
131+
verbose_name: 'Redirection URL',
132+
internal_name: "{$prefix}{$url_field_suffix}",
133+
validators: [new URLValidator()],
134+
help_text: 'The URL the user should be redirected to after successful login. ' .
135+
'Example: http://www.google.com',
136+
);
137+
$this->simultaneous_connect = new IntegerField(
138+
required: false,
139+
default: null,
140+
allow_null: true,
141+
minimum: 0,
142+
verbose_name: 'Simultaneous Connections',
143+
internal_name: "{$prefix}simultaneousconnect",
144+
help_text: 'The maximum number of simultaneous connections with this entry. Leave null for no limit. ' .
145+
'If using FreeRADIUS with Captive Portal you should leave this null.',
146+
);
147+
$this->expiration = new StringField(
148+
required: false,
149+
default: '',
150+
allow_empty: true,
151+
verbose_name: 'Expiration Date',
152+
internal_name: "{$prefix}expiration",
153+
help_text: 'The date when this account should expire. Required format: Mmm dd yyyy (e.g. Jan 01 2030).',
154+
);
155+
$this->session_timeout = new IntegerField(
156+
required: false,
157+
default: null,
158+
allow_null: true,
159+
minimum: 0,
160+
verbose_name: 'Session Timeout',
161+
internal_name: "{$prefix}sessiontimeout",
162+
help_text: 'The time this entry has until relogin (in seconds).',
163+
);
164+
$this->login_time = new StringField(
165+
required: false,
166+
default: '',
167+
allow_empty: true,
168+
verbose_name: 'Possible Login Times',
169+
internal_name: "{$prefix}logintime",
170+
validators: [
171+
new RegexValidator(
172+
pattern: '/^[a-zA-Z0-9,|-]*$/',
173+
error_msg: 'Value may only contain a-z, A-Z, 0-9, comma, vertical bar and hyphen.',
174+
),
175+
],
176+
help_text: 'The time when this entry should have access. Empty for no time restriction. ' .
177+
'Example: Wk0855-2305,Sa|Su2230-0230 ' .
178+
'(weekdays after 8:55 AM and before 11:05 PM | any time on Saturday | ' .
179+
'Sunday after 10:30 PM and before 02:30 AM).',
180+
);
181+
$this->amount_of_time = new IntegerField(
182+
required: false,
183+
default: null,
184+
allow_null: true,
185+
minimum: 0,
186+
verbose_name: 'Amount of Time',
187+
internal_name: "{$prefix}amountoftime",
188+
help_text: 'The amount of time this entry is allowed (in minutes) within the configured time period.',
189+
);
190+
$this->point_of_time = new StringField(
191+
required: false,
192+
default: 'Daily',
193+
choices: ['Daily', 'Weekly', 'Monthly', 'Forever'],
194+
verbose_name: 'Time Period',
195+
internal_name: "{$prefix}pointoftime",
196+
help_text: "The time period after which the 'Amount of Time' is reset.",
197+
);
198+
$this->max_total_octets = new IntegerField(
199+
required: false,
200+
default: null,
201+
allow_null: true,
202+
minimum: 0,
203+
verbose_name: 'Max Total Octets',
204+
internal_name: "{$prefix}maxtotaloctets",
205+
help_text: 'The amount of download and upload traffic (summarized) in megabytes (MB) for this entry. ' .
206+
'If using captive portal without periodic reauthentication enabled, this value must not exceed ' .
207+
'4095 due to protocol limitations.',
208+
);
209+
$this->max_total_octets_time_range = new StringField(
210+
required: false,
211+
default: 'daily',
212+
choices: ['daily', 'weekly', 'monthly', 'forever'],
213+
verbose_name: 'Max Total Octets Time Range',
214+
internal_name: "{$prefix}maxtotaloctetstimerange",
215+
help_text: 'The time period for the amount of download and upload traffic. ' .
216+
'This does not automatically reset the counter; you must configure a cronjob to reset it.',
217+
);
218+
$this->max_bandwidth_down = new IntegerField(
219+
required: false,
220+
default: null,
221+
allow_null: true,
222+
minimum: 0,
223+
maximum: 4294967,
224+
verbose_name: 'Max Bandwidth Down',
225+
internal_name: "{$prefix}maxbandwidthdown",
226+
help_text: 'The maximum bandwidth for download in kilobits (1000 bits) per second (Kbit/s).',
227+
);
228+
$this->max_bandwidth_up = new IntegerField(
229+
required: false,
230+
default: null,
231+
allow_null: true,
232+
minimum: 0,
233+
maximum: 4294967,
234+
verbose_name: 'Max Bandwidth Up',
235+
internal_name: "{$prefix}maxbandwidthup",
236+
help_text: 'The maximum bandwidth for upload in kilobits (1000 bits) per second (Kbit/s).',
237+
);
238+
$this->acct_interim_interval = new IntegerField(
239+
required: false,
240+
default: null,
241+
allow_null: true,
242+
minimum: 61,
243+
verbose_name: 'Accounting Interim Interval',
244+
internal_name: "{$prefix}acctinteriminterval",
245+
help_text: 'The interval in seconds which should elapse between interim-updates. ' .
246+
'Must be more than 60s and should not be less than 600s.',
247+
);
248+
$this->top_additional_options = new StringField(
249+
required: false,
250+
default: [],
251+
allow_empty: true,
252+
many: true,
253+
delimiter: '|',
254+
verbose_name: 'Additional Top Options',
255+
internal_name: "{$prefix}topadditionaloptions",
256+
help_text: 'Additional RADIUS attributes placed at the TOP of this entry. ' .
257+
'Each list entry becomes a separate line. For experts only.',
258+
);
259+
$this->check_items_additional_options = new StringField(
260+
required: false,
261+
default: [],
262+
allow_empty: true,
263+
many: true,
264+
delimiter: '|',
265+
verbose_name: 'Additional Check-Item Options',
266+
internal_name: "{$prefix}checkitemsadditionaloptions",
267+
help_text: 'Additional RADIUS check-item attributes for this entry. ' .
268+
'Each list entry becomes a separate attribute. For experts only.',
269+
);
270+
$this->reply_items_additional_options = new StringField(
271+
required: false,
272+
default: [],
273+
allow_empty: true,
274+
many: true,
275+
delimiter: '|',
276+
verbose_name: 'Additional Reply-Item Options',
277+
internal_name: "{$prefix}replyitemsadditionaloptions",
278+
help_text: 'Additional RADIUS reply-item attributes for this entry. ' .
279+
'Each list entry becomes a separate attribute. For experts only.',
280+
);
281+
}
282+
}

0 commit comments

Comments
 (0)