Development

This commit is contained in:
Matthew Grotke 2026-06-07 23:32:06 -04:00
parent db837af548
commit 64f83d683e
8 changed files with 237 additions and 26 deletions

View file

@ -18,14 +18,14 @@ bp = Blueprint(_PAGE, __name__)
USER_TYPE_CAPTIVE = 0
USER_TYPE_SUPPLICANT = 1
HASH_CLEARTEXT = 0
HASH_BCRYPT = 2
DIGEST_CYPHERTEXT_FERNET = 0
DIGEST_HASH_BCRYPT = 2
VALID_USER_TYPES = {USER_TYPE_CAPTIVE, USER_TYPE_SUPPLICANT}
HASH_FOR_USER_TYPE = {
USER_TYPE_CAPTIVE: HASH_BCRYPT,
USER_TYPE_SUPPLICANT: HASH_CLEARTEXT,
USER_TYPE_CAPTIVE: DIGEST_HASH_BCRYPT,
USER_TYPE_SUPPLICANT: DIGEST_CYPHERTEXT_FERNET,
}
@ -67,7 +67,7 @@ def _db_conn():
password TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
user_type INTEGER NOT NULL,
hash_type INTEGER NOT NULL,
digest_type INTEGER NOT NULL,
vlan TEXT NOT NULL DEFAULT '',
enabled INTEGER NOT NULL DEFAULT 1,
date_set INTEGER NOT NULL,
@ -95,12 +95,12 @@ def _get_by_index(conn, row_index):
return rows[row_index]
def _hash_password(plaintext, hash_type):
if hash_type == HASH_CLEARTEXT:
def _hash_password(plaintext, digest_type):
if digest_type == DIGEST_CYPHERTEXT_FERNET:
return encrypt_password(plaintext)
if hash_type == HASH_BCRYPT:
if digest_type == DIGEST_HASH_BCRYPT:
return bcrypt.hashpw(plaintext.encode(), bcrypt.gensalt()).decode()
raise ValueError(f"Unknown hash_type: {hash_type}")
raise ValueError(f"Unknown digest_type: {digest_type}")
def _parse_valid_for(value_str, unit_str):
@ -166,7 +166,7 @@ def addedit():
flash('Invalid user type.', 'error')
return redirect(f'/{_PAGE}')
hash_type = HASH_FOR_USER_TYPE[user_type]
digest_type = HASH_FOR_USER_TYPE[user_type]
vlan = sanitize.name(request.form.get('vlan', ''))
if not vlan:
@ -205,26 +205,26 @@ def addedit():
if password:
try:
hashed = _hash_password(password, hash_type)
hashed = _hash_password(password, digest_type)
except ValueError as exc:
conn.close()
flash(str(exc), 'error')
return redirect(f'/{_PAGE}')
stored_password = hashed
stored_hash_type = hash_type
stored_digest_type = digest_type
date_set = int(time.time())
else:
stored_password = existing['password']
stored_hash_type = existing['hash_type']
stored_digest_type = existing['digest_type']
date_set = existing['date_set']
try:
conn.execute(
"""UPDATE credentials
SET username=?, password=?, description=?, user_type=?, hash_type=?,
SET username=?, password=?, description=?, user_type=?, digest_type=?,
vlan=?, enabled=?, date_set=?, valid_for=?
WHERE id=?""",
(username, stored_password, description, user_type, stored_hash_type,
(username, stored_password, description, user_type, stored_digest_type,
vlan, int(enabled), date_set, valid_for, existing['id']),
)
conn.commit()
@ -242,7 +242,7 @@ def addedit():
flash(f"Auto-generated password for '{username}': {password}", 'success')
try:
hashed = _hash_password(password, hash_type)
hashed = _hash_password(password, digest_type)
except ValueError as exc:
conn.close()
flash(str(exc), 'error')
@ -251,9 +251,9 @@ def addedit():
try:
conn.execute(
"""INSERT INTO credentials
(username, password, description, user_type, hash_type, vlan, enabled, date_set, valid_for)
(username, password, description, user_type, digest_type, vlan, enabled, date_set, valid_for)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)""",
(username, hashed, description, user_type, hash_type,
(username, hashed, description, user_type, digest_type,
vlan, int(enabled), int(time.time()), valid_for),
)
conn.commit()

View file

@ -23,7 +23,7 @@ def _load_credentials():
password TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
user_type INTEGER NOT NULL,
hash_type INTEGER NOT NULL,
digest_type INTEGER NOT NULL,
vlan TEXT NOT NULL DEFAULT '',
enabled INTEGER NOT NULL DEFAULT 1,
date_set INTEGER NOT NULL,

View file

@ -59,10 +59,10 @@ def options_save():
@auth.require_level('administrator')
def auth_mode_save():
auth_mode = request.form.get('auth_mode', 'mab')
if auth_mode not in ('mab', 'eap_password', 'eap_credential'):
if auth_mode not in ('mab', 'eap_password', 'eap_certificate'):
flash('Invalid authentication mode.', 'error')
return redirect(f'/{_PAGE}')
if auth_mode in ('eap_password', 'eap_credential') and not PRO_LICENSE:
if auth_mode in ('eap_password', 'eap_certificate') and not PRO_LICENSE:
flash('This authentication mode requires a Routlin Pro license.', 'error')
return redirect(f'/{_PAGE}')
@ -101,7 +101,7 @@ def auth_mode_save():
after['default_session_seconds'] = dur_n * mult if dur_n > 0 else 0
except (ValueError, TypeError):
after['default_session_seconds'] = 0
elif auth_mode == 'eap_credential':
elif auth_mode == 'eap_certificate':
after['include_length'] = include_length
after['mab_first'] = mab_first
after.pop('eap_protocol', None)

View file

@ -96,7 +96,7 @@ def collect_tokens(cfg):
tokens['RADIUS_AUTH_MODE_OPTIONS'] = json.dumps([
{'value': 'mab', 'label': 'MAC Authentication Bypass (MAB)'},
{'value': 'eap_password', 'label': f'802.1X - Client Username/Password{pro_suffix}', 'disabled': pro_disabled},
{'value': 'eap_credential', 'label': f'802.1X - Client Certificate{pro_suffix}', 'disabled': pro_disabled},
{'value': 'eap_certificate', 'label': f'802.1X - Client Certificate{pro_suffix}', 'disabled': pro_disabled},
])
tokens['RADIUS_APPLY_TO'] = fr_opts.get('apply_to', 'all')
tokens['RADIUS_AP_IPS'] = json.dumps(fr_opts.get('ap_ips', []))