Add Days field to the TUI cert form
This commit is contained in:
@@ -791,15 +791,17 @@ class CertFormResult:
|
|||||||
email: str
|
email: str
|
||||||
password: str
|
password: str
|
||||||
confirmed: bool
|
confirmed: bool
|
||||||
|
days: str = ""
|
||||||
|
|
||||||
|
|
||||||
_FORM_FIELDS = ("cn", "email", "password")
|
_FORM_FIELDS = ("cn", "email", "days", "password")
|
||||||
_FORM_LABELS = {
|
_FORM_LABELS = {
|
||||||
"cn": "CN",
|
"cn": "CN",
|
||||||
"email": "Email",
|
"email": "Email",
|
||||||
|
"days": "Days (blank = EasyRSA default)",
|
||||||
"password": "Password",
|
"password": "Password",
|
||||||
}
|
}
|
||||||
_FORM_FIELD_Y = {"cn": 3, "email": 5, "password": 7}
|
_FORM_FIELD_Y = {"cn": 3, "email": 5, "days": 7, "password": 9}
|
||||||
|
|
||||||
|
|
||||||
def show_cert_form(
|
def show_cert_form(
|
||||||
@@ -807,6 +809,7 @@ def show_cert_form(
|
|||||||
cn: str = "",
|
cn: str = "",
|
||||||
email: str = "",
|
email: str = "",
|
||||||
password: str = "",
|
password: str = "",
|
||||||
|
days: str = "",
|
||||||
cn_readonly: bool = False,
|
cn_readonly: bool = False,
|
||||||
) -> CertFormResult:
|
) -> CertFormResult:
|
||||||
"""Modal cert-detail form.
|
"""Modal cert-detail form.
|
||||||
@@ -816,7 +819,7 @@ def show_cert_form(
|
|||||||
# Clamp to the screen so curses.newwin() can't raise "curses function
|
# Clamp to the screen so curses.newwin() can't raise "curses function
|
||||||
# returned NULL" outright on a terminal narrower/shorter than the usual
|
# returned NULL" outright on a terminal narrower/shorter than the usual
|
||||||
# 80x24 (e.g. a serial console with an unusually small viewport).
|
# 80x24 (e.g. a serial console with an unusually small viewport).
|
||||||
h, w = min(13, sh), min(62, sw)
|
h, w = min(15, sh), min(62, sw)
|
||||||
try:
|
try:
|
||||||
win = curses.newwin(h, w, max(0, (sh - h) // 2), max(0, (sw - w) // 2))
|
win = curses.newwin(h, w, max(0, (sh - h) // 2), max(0, (sw - w) // 2))
|
||||||
except curses.error:
|
except curses.error:
|
||||||
@@ -826,7 +829,7 @@ def show_cert_form(
|
|||||||
|
|
||||||
_BTN_CONTINUE = "_continue"
|
_BTN_CONTINUE = "_continue"
|
||||||
_BTN_CANCEL = "_cancel"
|
_BTN_CANCEL = "_cancel"
|
||||||
_BTN_Y = 9
|
_BTN_Y = 11
|
||||||
_btn_x_cont = (w - 26) // 2 # "[ Continue ]"=12 + gap=4 + "[ Cancel ]"=10 = 26
|
_btn_x_cont = (w - 26) // 2 # "[ Continue ]"=12 + gap=4 + "[ Cancel ]"=10 = 26
|
||||||
_btn_x_canc = _btn_x_cont + 16
|
_btn_x_canc = _btn_x_cont + 16
|
||||||
|
|
||||||
@@ -835,18 +838,32 @@ def show_cert_form(
|
|||||||
fields: Dict[str, InputField] = {
|
fields: Dict[str, InputField] = {
|
||||||
"cn": InputField(win, _FORM_FIELD_Y["cn"], 3, fw, initial=cn),
|
"cn": InputField(win, _FORM_FIELD_Y["cn"], 3, fw, initial=cn),
|
||||||
"email": InputField(win, _FORM_FIELD_Y["email"], 3, fw, initial=email),
|
"email": InputField(win, _FORM_FIELD_Y["email"], 3, fw, initial=email),
|
||||||
|
"days": InputField(win, _FORM_FIELD_Y["days"], 3, fw, initial=days,
|
||||||
|
allowed=string.digits),
|
||||||
"password": InputField(win, _FORM_FIELD_Y["password"], 3, fw,
|
"password": InputField(win, _FORM_FIELD_Y["password"], 3, fw,
|
||||||
initial=password if password else generate_password(),
|
initial=password if password else generate_password(),
|
||||||
mask=True),
|
mask=True),
|
||||||
}
|
}
|
||||||
focus = 0
|
focus = 0
|
||||||
|
error = ""
|
||||||
|
|
||||||
def _submit() -> CertFormResult:
|
def _submit() -> Optional[CertFormResult]:
|
||||||
|
nonlocal error
|
||||||
|
try:
|
||||||
|
# Validate with the same resolver the CLI and .conf use, so what
|
||||||
|
# counts as valid never diverges between entry points.
|
||||||
|
days_value = resolve_cert_days(fields["days"].value, "Days")
|
||||||
|
except ConfigError:
|
||||||
|
# The resolver's full sentence does not fit a 62-column window.
|
||||||
|
error = "Days must be a positive number, or blank to inherit"
|
||||||
|
return None
|
||||||
|
error = ""
|
||||||
final_cn = cn if cn_readonly else fields["cn"].value
|
final_cn = cn if cn_readonly else fields["cn"].value
|
||||||
return CertFormResult(
|
return CertFormResult(
|
||||||
cn=final_cn,
|
cn=final_cn,
|
||||||
email=fields["email"].value,
|
email=fields["email"].value,
|
||||||
password=fields["password"].value,
|
password=fields["password"].value,
|
||||||
|
days=days_value,
|
||||||
confirmed=True,
|
confirmed=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -887,8 +904,11 @@ def show_cert_form(
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
hint = "Tab=next F5=regen pwd Ctrl-G=confirm Esc=cancel"
|
hint = "Tab=next F5=regen pwd Ctrl-G=confirm Esc=cancel"
|
||||||
|
hint_attr = curses.color_pair(COLOR_DISABLED)
|
||||||
|
if error:
|
||||||
|
hint, hint_attr = error, curses.color_pair(COLOR_ERROR)
|
||||||
try:
|
try:
|
||||||
win.addstr(h - 2, 2, hint[:w - 4], curses.color_pair(COLOR_DISABLED))
|
win.addstr(h - 2, 2, hint[:w - 4], hint_attr)
|
||||||
except curses.error:
|
except curses.error:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -913,8 +933,11 @@ def show_cert_form(
|
|||||||
curses.curs_set(0)
|
curses.curs_set(0)
|
||||||
return CertFormResult(cn=cn, email="", password="", confirmed=False)
|
return CertFormResult(cn=cn, email="", password="", confirmed=False)
|
||||||
if key == 0x07: # Ctrl-G: immediate submit
|
if key == 0x07: # Ctrl-G: immediate submit
|
||||||
curses.curs_set(0)
|
result = _submit()
|
||||||
return _submit()
|
if result is not None:
|
||||||
|
curses.curs_set(0)
|
||||||
|
return result
|
||||||
|
continue
|
||||||
if key in (9, curses.KEY_DOWN): # Tab / Down: next
|
if key in (9, curses.KEY_DOWN): # Tab / Down: next
|
||||||
focus = (focus + 1) % len(active)
|
focus = (focus + 1) % len(active)
|
||||||
continue
|
continue
|
||||||
@@ -934,9 +957,13 @@ def show_cert_form(
|
|||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
if key in (10, 13, curses.KEY_ENTER, ord(" ")):
|
if key in (10, 13, curses.KEY_ENTER, ord(" ")):
|
||||||
curses.curs_set(0)
|
|
||||||
if current == _BTN_CONTINUE:
|
if current == _BTN_CONTINUE:
|
||||||
return _submit()
|
result = _submit()
|
||||||
|
if result is not None:
|
||||||
|
curses.curs_set(0)
|
||||||
|
return result
|
||||||
|
continue
|
||||||
|
curses.curs_set(0)
|
||||||
return CertFormResult(cn=cn, email="", password="", confirmed=False)
|
return CertFormResult(cn=cn, email="", password="", confirmed=False)
|
||||||
else:
|
else:
|
||||||
if key in (10, 13, curses.KEY_ENTER):
|
if key in (10, 13, curses.KEY_ENTER):
|
||||||
|
|||||||
@@ -115,11 +115,11 @@ def test_show_cert_form_uses_erase_not_clear():
|
|||||||
|
|
||||||
|
|
||||||
def test_show_cert_form_confirm():
|
def test_show_cert_form_confirm():
|
||||||
"""Enter advances through the 3 fields to the Continue button; Enter on
|
"""Enter advances through the 4 fields to the Continue button; Enter on
|
||||||
Continue confirms. That's 4 Enter keypresses total."""
|
Continue confirms. That's 5 Enter keypresses total."""
|
||||||
stdscr = _make_stdscr()
|
stdscr = _make_stdscr()
|
||||||
win = _make_win(rows=20, cols=70)
|
win = _make_win(rows=20, cols=70)
|
||||||
win.getch.side_effect = [10, 10, 10, 10]
|
win.getch.side_effect = [10, 10, 10, 10, 10]
|
||||||
with patch("curses.newwin", return_value=win):
|
with patch("curses.newwin", return_value=win):
|
||||||
result = show_cert_form(stdscr, cn="bob", email="bob@example.com")
|
result = show_cert_form(stdscr, cn="bob", email="bob@example.com")
|
||||||
assert isinstance(result, CertFormResult)
|
assert isinstance(result, CertFormResult)
|
||||||
@@ -130,11 +130,11 @@ def test_show_cert_form_confirm():
|
|||||||
|
|
||||||
|
|
||||||
def test_show_cert_form_cancel_button():
|
def test_show_cert_form_cancel_button():
|
||||||
"""Tab to the Cancel button (4 Tabs from start) and press Enter cancels."""
|
"""Tab to the Cancel button (5 Tabs from start) and press Enter cancels."""
|
||||||
stdscr = _make_stdscr()
|
stdscr = _make_stdscr()
|
||||||
win = _make_win(rows=20, cols=70)
|
win = _make_win(rows=20, cols=70)
|
||||||
# Tab×4: cn→email→password→Continue→Cancel, then Enter
|
# Tab×5: cn→email→days→password→Continue→Cancel, then Enter
|
||||||
win.getch.side_effect = [9, 9, 9, 9, 10]
|
win.getch.side_effect = [9, 9, 9, 9, 9, 10]
|
||||||
with patch("curses.newwin", return_value=win):
|
with patch("curses.newwin", return_value=win):
|
||||||
result = show_cert_form(stdscr, cn="alice", email="alice@example.com")
|
result = show_cert_form(stdscr, cn="alice", email="alice@example.com")
|
||||||
assert isinstance(result, CertFormResult)
|
assert isinstance(result, CertFormResult)
|
||||||
@@ -142,7 +142,7 @@ def test_show_cert_form_cancel_button():
|
|||||||
|
|
||||||
|
|
||||||
def test_show_cert_form_clamps_to_narrow_screen():
|
def test_show_cert_form_clamps_to_narrow_screen():
|
||||||
"""The form's fixed 13x62 size must not exceed a smaller-than-usual
|
"""The form's fixed 15x62 size must not exceed a smaller-than-usual
|
||||||
screen, which would make curses.newwin() raise."""
|
screen, which would make curses.newwin() raise."""
|
||||||
stdscr = _make_stdscr(rows=10, cols=40)
|
stdscr = _make_stdscr(rows=10, cols=40)
|
||||||
win = _make_win()
|
win = _make_win()
|
||||||
@@ -156,6 +156,58 @@ def test_show_cert_form_clamps_to_narrow_screen():
|
|||||||
assert x >= 0
|
assert x >= 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_show_cert_form_returns_days():
|
||||||
|
stdscr = _make_stdscr()
|
||||||
|
win = _make_win(rows=20, cols=70)
|
||||||
|
win.getch.side_effect = [10, 10, 10, 10, 10]
|
||||||
|
with patch("curses.newwin", return_value=win):
|
||||||
|
result = show_cert_form(stdscr, cn="bob", email="b@c.d", days="90")
|
||||||
|
assert result.confirmed is True
|
||||||
|
assert result.days == "90"
|
||||||
|
|
||||||
|
|
||||||
|
def test_show_cert_form_blank_days_means_inherit():
|
||||||
|
stdscr = _make_stdscr()
|
||||||
|
win = _make_win(rows=20, cols=70)
|
||||||
|
win.getch.side_effect = [10, 10, 10, 10, 10]
|
||||||
|
with patch("curses.newwin", return_value=win):
|
||||||
|
result = show_cert_form(stdscr, cn="bob", email="b@c.d", days="")
|
||||||
|
assert result.confirmed is True
|
||||||
|
assert result.days == ""
|
||||||
|
|
||||||
|
|
||||||
|
def test_show_cert_form_days_field_is_digits_only():
|
||||||
|
# cn is read-only here, so focus starts on email: Tab once to reach days.
|
||||||
|
stdscr = _make_stdscr()
|
||||||
|
win = _make_win(rows=20, cols=70)
|
||||||
|
win.getch.side_effect = [9] + [ord(c) for c in "9a0!"] + [10, 10, 10]
|
||||||
|
with patch("curses.newwin", return_value=win):
|
||||||
|
result = show_cert_form(stdscr, cn="bob", email="b@c.d", days="",
|
||||||
|
cn_readonly=True)
|
||||||
|
assert result.confirmed is True
|
||||||
|
assert result.days == "90"
|
||||||
|
|
||||||
|
|
||||||
|
def test_show_cert_form_refuses_zero_days():
|
||||||
|
# Submitting "0" must keep the form open rather than closing and failing
|
||||||
|
# later inside EasyRSA. Enter×5 tries to submit, Esc then cancels.
|
||||||
|
stdscr = _make_stdscr()
|
||||||
|
win = _make_win(rows=20, cols=70)
|
||||||
|
win.getch.side_effect = [10, 10, 10, 10, 10, 27]
|
||||||
|
with patch("curses.newwin", return_value=win):
|
||||||
|
result = show_cert_form(stdscr, cn="bob", email="b@c.d", days="0")
|
||||||
|
assert result.confirmed is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_show_cert_form_normalises_leading_zero_days():
|
||||||
|
stdscr = _make_stdscr()
|
||||||
|
win = _make_win(rows=20, cols=70)
|
||||||
|
win.getch.side_effect = [10, 10, 10, 10, 10]
|
||||||
|
with patch("curses.newwin", return_value=win):
|
||||||
|
result = show_cert_form(stdscr, cn="bob", email="b@c.d", days="090")
|
||||||
|
assert result.days == "90"
|
||||||
|
|
||||||
|
|
||||||
def test_show_cert_form_newwin_failure_returns_cancelled():
|
def test_show_cert_form_newwin_failure_returns_cancelled():
|
||||||
"""If curses.newwin() still fails despite clamping, show_cert_form must
|
"""If curses.newwin() still fails despite clamping, show_cert_form must
|
||||||
degrade to an unconfirmed result rather than crash the whole app."""
|
degrade to an unconfirmed result rather than crash the whole app."""
|
||||||
|
|||||||
Reference in New Issue
Block a user