Development

This commit is contained in:
Matthew Grotke 2026-06-08 01:08:24 -04:00
parent 43c4cf380d
commit f011594b04
10 changed files with 163 additions and 46 deletions

View file

@ -62,6 +62,14 @@ def portal_save():
except (ValueError, TypeError): except (ValueError, TypeError):
duration = 0 duration = 0
try:
exp_n = int(request.form.get('default_expiration_value', '0').strip() or '0')
exp_unit = request.form.get('default_expiration_unit', 'hours')
mult = {'hours': 3600, 'days': 86400}.get(exp_unit, 3600)
expiration = exp_n * mult if exp_n > 0 else 0
except (ValueError, TypeError):
expiration = 0
after = { after = {
**existing, **existing,
'portal_splash_title': splash_title, 'portal_splash_title': splash_title,
@ -69,6 +77,7 @@ def portal_save():
'portal_terms': terms, 'portal_terms': terms,
'require_username_password': require_upw, 'require_username_password': require_upw,
'default_session_seconds': duration, 'default_session_seconds': duration,
'default_expiration_seconds': expiration,
} }
vlan['captive_portal'] = after vlan['captive_portal'] = after

View file

@ -177,7 +177,7 @@
"input_type": "number", "input_type": "number",
"min": 0, "min": 0,
"value": "0", "value": "0",
"hint": "How long portal access lasts after authentication. 0 = no expiration." "hint": "How long portal access lasts after authentication. 0 = no session limit."
}, },
{ {
"type": "field", "type": "field",
@ -191,6 +191,31 @@
} }
] ]
}, },
{
"type": "field_row",
"cols": 2,
"items": [
{
"type": "field",
"label": "Default Expiration Duration",
"name": "default_expiration_value",
"input_type": "number",
"min": 0,
"value": "0",
"hint": "How long after creation an account is valid before it permanently expires. 0 = never expires."
},
{
"type": "field",
"label": "Unit",
"name": "default_expiration_unit",
"input_type": "select",
"options": [
{"value": "hours", "label": "Hours"},
{"value": "days", "label": "Days"}
]
}
]
},
{ {
"type": "button_row", "type": "button_row",
"items": [ "items": [

View file

@ -39,6 +39,7 @@ def collect_tokens(cfg):
terms = cp.get('portal_terms', vlan.get('portal_terms', [])) terms = cp.get('portal_terms', vlan.get('portal_terms', []))
require_upw = cp.get('require_username_password', vlan.get('require_username_password', False)) require_upw = cp.get('require_username_password', vlan.get('require_username_password', False))
duration = cp.get('default_session_seconds', vlan.get('default_session_seconds', 0)) duration = cp.get('default_session_seconds', vlan.get('default_session_seconds', 0))
expiration = cp.get('default_expiration_seconds', vlan.get('default_expiration_seconds', 0))
n = len(terms) n = len(terms)
display_rows.append({ display_rows.append({
'vlan_name': vlan['name'], 'vlan_name': vlan['name'],
@ -49,6 +50,7 @@ def collect_tokens(cfg):
'require_upw': require_upw, 'require_upw': require_upw,
'require_username_password': require_upw, 'require_username_password': require_upw,
'default_session_seconds': duration, 'default_session_seconds': duration,
'default_expiration_seconds': expiration,
'session_display': _format_session(duration), 'session_display': _format_session(duration),
}) })

View file

@ -187,8 +187,12 @@ def addedit():
enabled = 'enabled' in request.form enabled = 'enabled' in request.form
session_seconds = _parse_session_seconds( session_seconds = _parse_session_seconds(
request.form.get('session_seconds_value', ''), request.form.get('session_duration_value', ''),
request.form.get('session_seconds_unit', 'hours'), request.form.get('session_duration_unit', 'hours'),
)
expires_seconds = _parse_session_seconds(
request.form.get('expiration_duration_value', ''),
request.form.get('expiration_duration_unit', 'hours'),
) )
if not username: if not username:
@ -223,10 +227,10 @@ def addedit():
conn.execute( conn.execute(
"""UPDATE credentials """UPDATE credentials
SET username=?, password=?, description=?, user_type=?, digest_type=?, SET username=?, password=?, description=?, user_type=?, digest_type=?,
vlan=?, enabled=?, date_set=?, session_seconds=? vlan=?, enabled=?, date_set=?, session_seconds=?, expires_seconds=?
WHERE id=?""", WHERE id=?""",
(username, stored_password, description, user_type, stored_digest_type, (username, stored_password, description, user_type, stored_digest_type,
vlan, int(enabled), date_set, session_seconds, existing['id']), vlan, int(enabled), date_set, session_seconds, expires_seconds, existing['id']),
) )
conn.commit() conn.commit()
except sqlite3.IntegrityError: except sqlite3.IntegrityError:
@ -255,7 +259,7 @@ def addedit():
(username, password, description, user_type, digest_type, vlan, enabled, date_set, session_seconds, expires_seconds) (username, password, description, user_type, digest_type, vlan, enabled, date_set, session_seconds, expires_seconds)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""", VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
(username, hashed, description, user_type, digest_type, (username, hashed, description, user_type, digest_type,
vlan, int(enabled), int(time.time()), session_seconds, 0), vlan, int(enabled), int(time.time()), session_seconds, expires_seconds),
) )
conn.commit() conn.commit()
except sqlite3.IntegrityError: except sqlite3.IntegrityError:

View file

@ -76,7 +76,7 @@
{ {
"type": "card", "type": "card",
"id": "add-form", "id": "add-form",
"label": "Add Credential", "label": "Add User Account",
"client_requirement": "client_is_administrator+", "client_requirement": "client_is_administrator+",
"items": [ "items": [
{ {
@ -91,13 +91,14 @@
}, },
{ {
"type": "field", "type": "field",
"label": "Credential Active", "label": "Account Active",
"name": "enabled", "name": "enabled",
"input_type": "checkbox" "input_type": "checkbox",
"value": "true"
}, },
{ {
"type": "raw_html", "type": "raw_html",
"html": "<script id=\"captive-vlan-data\" type=\"application/json\">%CAPTIVE_VLAN_OPTIONS%</script><script id=\"page-flags\" type=\"application/json\">{\"pro\": %PRO_LICENSE_JS%, \"radius_session_seconds\": %RADIUS_DEFAULT_SESSION_SECONDS_JS%}</script>" "html": "<script id=\"captive-vlan-data\" type=\"application/json\">%CAPTIVE_VLAN_OPTIONS%</script><script id=\"page-flags\" type=\"application/json\">{\"pro\": %PRO_LICENSE_JS%, \"radius_session_seconds\": %RADIUS_DEFAULT_SESSION_SECONDS_JS%, \"radius_expiration_seconds\": %RADIUS_DEFAULT_EXPIRATION_SECONDS_JS%}</script>"
}, },
{ {
"type": "field", "type": "field",
@ -170,17 +171,42 @@
"items": [ "items": [
{ {
"type": "field", "type": "field",
"label": "Valid For", "label": "Session Duration",
"name": "session_seconds_value", "name": "session_duration_value",
"input_type": "number", "input_type": "number",
"min": 0, "min": 0,
"value": "0", "value": "0",
"hint": "How long this credential is valid after creation. 0 = no expiration." "hint": "0 = no session limit"
}, },
{ {
"type": "field", "type": "field",
"label": "Unit", "label": "Unit",
"name": "session_seconds_unit", "name": "session_duration_unit",
"input_type": "select",
"options": [
{"value": "hours", "label": "Hours"},
{"value": "days", "label": "Days"}
]
}
]
},
{
"type": "field_row",
"cols": 2,
"items": [
{
"type": "field",
"label": "Expiration Duration",
"name": "expiration_duration_value",
"input_type": "number",
"min": 0,
"value": "0",
"hint": "How long after creation an account is valid before it permanently expires. 0 = never expires."
},
{
"type": "field",
"label": "Unit",
"name": "expiration_duration_unit",
"input_type": "select", "input_type": "select",
"options": [ "options": [
{"value": "hours", "label": "Hours"}, {"value": "hours", "label": "Hours"},

View file

@ -71,6 +71,9 @@ def collect_tokens(cfg):
tokens['RADIUS_DEFAULT_SESSION_SECONDS_JS'] = str( tokens['RADIUS_DEFAULT_SESSION_SECONDS_JS'] = str(
cfg.get('radius', {}).get('options', {}).get('default_session_seconds', 0) or 0 cfg.get('radius', {}).get('options', {}).get('default_session_seconds', 0) or 0
) )
tokens['RADIUS_DEFAULT_EXPIRATION_SECONDS_JS'] = str(
cfg.get('radius', {}).get('options', {}).get('default_expiration_seconds', 0) or 0
)
vlans = [v for v in cfg.get('vlans', []) if not v.get('is_vpn')] vlans = [v for v in cfg.get('vlans', []) if not v.get('is_vpn')]
tokens['VLAN_OPTIONS'] = json.dumps( tokens['VLAN_OPTIONS'] = json.dumps(
@ -88,6 +91,8 @@ def collect_tokens(cfg):
v.get('require_username_password', False)), v.get('require_username_password', False)),
'default_session_seconds': v.get('captive_portal', {}).get('default_session_seconds', 'default_session_seconds': v.get('captive_portal', {}).get('default_session_seconds',
v.get('default_session_seconds', 0)), v.get('default_session_seconds', 0)),
'default_expiration_seconds': v.get('captive_portal', {}).get('default_expiration_seconds',
v.get('default_expiration_seconds', 0)),
} }
for v in captive_vlans for v in captive_vlans
] ]

View file

@ -101,6 +101,13 @@ def auth_mode_save():
after['default_session_seconds'] = dur_n * mult if dur_n > 0 else 0 after['default_session_seconds'] = dur_n * mult if dur_n > 0 else 0
except (ValueError, TypeError): except (ValueError, TypeError):
after['default_session_seconds'] = 0 after['default_session_seconds'] = 0
try:
exp_n = int(request.form.get('default_expiration_value', '0').strip() or '0')
exp_unit = request.form.get('default_expiration_unit', 'hours')
mult = {'hours': 3600, 'days': 86400}.get(exp_unit, 3600)
after['default_expiration_seconds'] = exp_n * mult if exp_n > 0 else 0
except (ValueError, TypeError):
after['default_expiration_seconds'] = 0
elif auth_mode == 'eap_certificate': elif auth_mode == 'eap_certificate':
after['include_length'] = include_length after['include_length'] = include_length
after['mab_first'] = mab_first after['mab_first'] = mab_first
@ -108,6 +115,7 @@ def auth_mode_save():
after.pop('tunneled_reply', None) after.pop('tunneled_reply', None)
after.pop('inner_protocol', None) after.pop('inner_protocol', None)
after.pop('default_session_seconds', None) after.pop('default_session_seconds', None)
after.pop('default_expiration_seconds', None)
else: # mab else: # mab
after.pop('eap_protocol', None) after.pop('eap_protocol', None)
after.pop('tunneled_reply', None) after.pop('tunneled_reply', None)
@ -115,6 +123,7 @@ def auth_mode_save():
after.pop('include_length', None) after.pop('include_length', None)
after.pop('mab_first', None) after.pop('mab_first', None)
after.pop('default_session_seconds', None) after.pop('default_session_seconds', None)
after.pop('default_expiration_seconds', None)
cfg.setdefault('radius', {})['options'] = after cfg.setdefault('radius', {})['options'] = after
changes = config_utils.diff_fields(before, after) changes = config_utils.diff_fields(before, after)

View file

@ -225,7 +225,7 @@
"input_type": "number", "input_type": "number",
"min": 0, "min": 0,
"value": "%RADIUS_DEFAULT_SESSION_VALUE%", "value": "%RADIUS_DEFAULT_SESSION_VALUE%",
"hint": "How long a client session lasts before reauthentication is required. 0 = no expiration." "hint": "How long a client session lasts before reauthentication is required. 0 = no session limit."
}, },
{ {
"type": "field", "type": "field",
@ -240,6 +240,32 @@
} }
] ]
}, },
{
"type": "field_row",
"cols": 2,
"items": [
{
"type": "field",
"label": "Default Expiration Duration",
"name": "default_expiration_value",
"input_type": "number",
"min": 0,
"value": "%RADIUS_DEFAULT_EXPIRATION_VALUE%",
"hint": "How long after creation an account is valid before it permanently expires. 0 = never expires."
},
{
"type": "field",
"label": "Unit",
"name": "default_expiration_unit",
"input_type": "select",
"value": "%RADIUS_DEFAULT_EXPIRATION_UNIT%",
"options": [
{"value": "hours", "label": "Hours"},
{"value": "days", "label": "Days"}
]
}
]
},
{ {
"type": "raw_html", "type": "raw_html",
"html": "</div>" "html": "</div>"

View file

@ -138,6 +138,17 @@ def collect_tokens(cfg):
tokens['RADIUS_DEFAULT_SESSION_VALUE'] = '0' tokens['RADIUS_DEFAULT_SESSION_VALUE'] = '0'
tokens['RADIUS_DEFAULT_SESSION_UNIT'] = 'hours' tokens['RADIUS_DEFAULT_SESSION_UNIT'] = 'hours'
exps = fr_opts.get('default_expiration_seconds', 0) or 0
if exps >= 86400 and exps % 86400 == 0:
tokens['RADIUS_DEFAULT_EXPIRATION_VALUE'] = str(exps // 86400)
tokens['RADIUS_DEFAULT_EXPIRATION_UNIT'] = 'days'
elif exps > 0:
tokens['RADIUS_DEFAULT_EXPIRATION_VALUE'] = str(max(1, round(exps / 3600)))
tokens['RADIUS_DEFAULT_EXPIRATION_UNIT'] = 'hours'
else:
tokens['RADIUS_DEFAULT_EXPIRATION_VALUE'] = '0'
tokens['RADIUS_DEFAULT_EXPIRATION_UNIT'] = 'hours'
vlans = cfg.get('vlans', []) vlans = cfg.get('vlans', [])
default_vlan = next((v['name'] for v in vlans if v.get('radius_default') is True), '') default_vlan = next((v['name'] for v in vlans if v.get('radius_default') is True), '')
vlan_options = [{'value': '', 'label': 'None (reject unknown devices)'}] vlan_options = [{'value': '', 'label': 'None (reject unknown devices)'}]

View file

@ -52,7 +52,7 @@ def _load_supplicant_credentials():
conn = sqlite3.connect(str(CREDENTIALS_DB_FILE)) conn = sqlite3.connect(str(CREDENTIALS_DB_FILE))
conn.row_factory = sqlite3.Row conn.row_factory = sqlite3.Row
rows = conn.execute( rows = conn.execute(
"SELECT username, password, digest_type, vlan, session_seconds, expires_seconds FROM credentials" "SELECT username, password, digest_type, vlan, session_seconds, expires_seconds, date_set FROM credentials"
" WHERE user_type=? AND enabled=1", " WHERE user_type=? AND enabled=1",
(USER_TYPE_SUPPLICANT,) (USER_TYPE_SUPPLICANT,)
).fetchall() ).fetchall()
@ -145,7 +145,7 @@ def _supplicant_reply_attrs(cred, default_session_seconds, vlan_id, vlan):
attrs.append(f" Session-Timeout = {session}") attrs.append(f" Session-Timeout = {session}")
expires = cred.get('expires_seconds') or 0 expires = cred.get('expires_seconds') or 0
if expires: if expires:
dt = datetime.datetime.fromtimestamp(expires) dt = datetime.datetime.fromtimestamp((cred.get('date_set') or 0) + expires)
attrs.append(f" Expiration := \"{dt.strftime('%b %d %Y %H:%M:%S')}\"") attrs.append(f" Expiration := \"{dt.strftime('%b %d %Y %H:%M:%S')}\"")
return attrs return attrs