Skip to content

Commit d417c4c

Browse files
committed
fix(webui): fail closed on incomplete auth credentials
Supplying only --auth-user or only --auth-pass (or only one of SKILLOPT_WEBUI_USER / SKILLOPT_WEBUI_PASS) previously left auth=None and still launched the UI — a deployment could expose the training controls without login. Now reject before building/launching (sys.exit 1); launch() is never called for incomplete credentials. Added user-only / pass-only / env-incomplete regressions.
1 parent 6f0030d commit d417c4c

2 files changed

Lines changed: 68 additions & 3 deletions

File tree

‎skillopt_webui/app.py‎

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -700,9 +700,18 @@ def main():
700700

701701
auth_user = args.auth_user or os.environ.get("SKILLOPT_WEBUI_USER")
702702
auth_pass = args.auth_pass or os.environ.get("SKILLOPT_WEBUI_PASS")
703-
auth = None
704-
if auth_user and auth_pass:
705-
auth = (auth_user, auth_pass)
703+
# Fail-closed: authentication requires BOTH credentials. Supplying only a
704+
# username or only a password must not silently launch the UI unauthenticated
705+
# (a deployment could expose the training controls without login).
706+
if bool(auth_user) != bool(auth_pass):
707+
print(
708+
"SKILLOPT_WEBUI authentication requires BOTH --auth-user and "
709+
"--auth-pass (or SKILLOPT_WEBUI_USER and SKILLOPT_WEBUI_PASS). "
710+
"Refusing to start with incomplete credentials.",
711+
file=sys.stderr,
712+
)
713+
sys.exit(1)
714+
auth = (auth_user, auth_pass) if auth_user else None
706715

707716
app = build_ui()
708717
launch_kwargs = build_launch_kwargs(args.host, args.port, args.share)

‎tests/test_webui_security.py‎

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,3 +157,59 @@ def test_scan_outputs_allows_valid_subdir(webui, tmp_path, monkeypatch):
157157
except ValueError:
158158
contained = False
159159
assert contained, "Valid subdirectory must pass containment check"
160+
161+
162+
def test_main_rejects_incomplete_cli_auth_user_only(webui, monkeypatch):
163+
"""--auth-user without --auth-pass must fail closed (never launch)."""
164+
webui_mod = webui
165+
launcher = mock.MagicMock()
166+
app_mock = mock.MagicMock()
167+
app_mock.launch = launcher
168+
monkeypatch.setattr(webui_mod, "build_ui", lambda: app_mock)
169+
monkeypatch.setattr(sys, "argv", ["app.py", "--host", "0.0.0.0", "--auth-user", "admin"])
170+
with pytest.raises(SystemExit):
171+
webui_mod.main()
172+
launcher.assert_not_called()
173+
174+
175+
def test_main_rejects_incomplete_cli_auth_pass_only(webui, monkeypatch):
176+
"""--auth-pass without --auth-user must fail closed (never launch)."""
177+
webui_mod = webui
178+
launcher = mock.MagicMock()
179+
app_mock = mock.MagicMock()
180+
app_mock.launch = launcher
181+
monkeypatch.setattr(webui_mod, "build_ui", lambda: app_mock)
182+
monkeypatch.setattr(sys, "argv", ["app.py", "--host", "0.0.0.0", "--auth-pass", "s3cret"])
183+
with pytest.raises(SystemExit):
184+
webui_mod.main()
185+
launcher.assert_not_called()
186+
187+
188+
def test_main_rejects_incomplete_env_auth_user_only(webui, monkeypatch):
189+
"""Only SKILLOPT_WEBUI_USER set must fail closed (never launch)."""
190+
webui_mod = webui
191+
launcher = mock.MagicMock()
192+
app_mock = mock.MagicMock()
193+
app_mock.launch = launcher
194+
monkeypatch.setattr(webui_mod, "build_ui", lambda: app_mock)
195+
monkeypatch.setattr(sys, "argv", ["app.py", "--host", "0.0.0.0"])
196+
monkeypatch.setenv("SKILLOPT_WEBUI_USER", "envuser")
197+
monkeypatch.delenv("SKILLOPT_WEBUI_PASS", raising=False)
198+
with pytest.raises(SystemExit):
199+
webui_mod.main()
200+
launcher.assert_not_called()
201+
202+
203+
def test_main_rejects_incomplete_env_auth_pass_only(webui, monkeypatch):
204+
"""Only SKILLOPT_WEBUI_PASS set must fail closed (never launch)."""
205+
webui_mod = webui
206+
launcher = mock.MagicMock()
207+
app_mock = mock.MagicMock()
208+
app_mock.launch = launcher
209+
monkeypatch.setattr(webui_mod, "build_ui", lambda: app_mock)
210+
monkeypatch.setattr(sys, "argv", ["app.py", "--host", "0.0.0.0"])
211+
monkeypatch.setenv("SKILLOPT_WEBUI_PASS", "envpass")
212+
monkeypatch.delenv("SKILLOPT_WEBUI_USER", raising=False)
213+
with pytest.raises(SystemExit):
214+
webui_mod.main()
215+
launcher.assert_not_called()

0 commit comments

Comments
 (0)