fix(fam-core): 登录密码不再硬编码兜底(fail-closed) + 会话有效期改 2 小时
之前 FAM_AUTH_USER/FAM_AUTH_PASS 没配置时会退回代码里写死的 ericwyuan/ iLoveJava5——这两个值跟 NAS SSH 密码是同一个,且这个登录页已经通过 frp 暴露在 公网上。.env 万一没配置好(这个项目里已经发生过好几次"忘了 source .env"的 情况),公网入口就会用一个和 SSH 密码相同、且写在源码里的已知密码兜底,双重 风险叠一起。 现在改成 fail-closed:FAM_AUTH_USER/FAM_AUTH_PASS 只要有一个没配置,直接拒绝 所有登录(打一条 ERROR 日志提醒去配 .env),不再有任何硬编码默认值。 会话有效期从 7 天改成 2 小时(按用户要求)。 新增 test_auth.py 11 个用例覆盖:未配置时 fail-closed、正确/错误凭据校验、 只配了一半也要拒绝、session 过期判定、白名单路径匹配。 已实测验证:当前 .env 里 FAM_AUTH_USER/PASS 仍是配置好的(还是 ericwyuan/ iLoveJava5,这个值本身没改——只是不再硬编码在代码里,运维如果想换成不同于 SSH 的密码,现在改 .env 就行,不用改代码),登录/拒绝错误密码都验证正常, cookie Max-Age=7200 确认生效。
This commit is contained in:
106
fam-core/tests/test_auth.py
Normal file
106
fam-core/tests/test_auth.py
Normal file
@@ -0,0 +1,106 @@
|
||||
import os
|
||||
import time
|
||||
|
||||
import pytest
|
||||
|
||||
from fam_core import auth
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _clean_env_and_sessions(monkeypatch):
|
||||
"""每个用例前清掉环境变量和进程内 session 表,用例之间不互相污染。"""
|
||||
monkeypatch.delenv('FAM_AUTH_USER', raising=False)
|
||||
monkeypatch.delenv('FAM_AUTH_PASS', raising=False)
|
||||
auth._sessions.clear()
|
||||
auth._warned_unconfigured = False
|
||||
yield
|
||||
auth._sessions.clear()
|
||||
|
||||
|
||||
def test_check_credential_fails_closed_when_unconfigured():
|
||||
"""核心诉求: .env 没配置 FAM_AUTH_USER/FAM_AUTH_PASS 时必须拒绝所有登录,
|
||||
不能退回任何硬编码默认账号密码(这两个变量跟 NAS SSH 密码是同一个值,公网
|
||||
入口不能有"没配置就用已知密码兜底"这种行为)。"""
|
||||
assert auth._check_credential('ericwyuan', 'iLoveJava5') is False
|
||||
assert auth._check_credential('anything', 'anything') is False
|
||||
assert auth._check_credential('', '') is False
|
||||
|
||||
|
||||
def test_check_credential_succeeds_with_matching_configured_values(monkeypatch):
|
||||
monkeypatch.setenv('FAM_AUTH_USER', 'testuser')
|
||||
monkeypatch.setenv('FAM_AUTH_PASS', 'testpass')
|
||||
assert auth._check_credential('testuser', 'testpass') is True
|
||||
|
||||
|
||||
def test_check_credential_rejects_wrong_password_when_configured(monkeypatch):
|
||||
monkeypatch.setenv('FAM_AUTH_USER', 'testuser')
|
||||
monkeypatch.setenv('FAM_AUTH_PASS', 'testpass')
|
||||
assert auth._check_credential('testuser', 'wrongpass') is False
|
||||
assert auth._check_credential('wronguser', 'testpass') is False
|
||||
|
||||
|
||||
def test_check_credential_fails_closed_when_only_one_var_set(monkeypatch):
|
||||
"""只配了一半(比如账号忘配密码)也要 fail closed,不能退化成"密码随便"。"""
|
||||
monkeypatch.setenv('FAM_AUTH_USER', 'testuser')
|
||||
assert auth._check_credential('testuser', '') is False
|
||||
assert auth._check_credential('testuser', 'anything') is False
|
||||
|
||||
|
||||
def test_session_ttl_is_two_hours():
|
||||
assert auth._SESSION_TTL == 2 * 3600
|
||||
|
||||
|
||||
def test_is_authed_true_within_ttl():
|
||||
tok = 'sometoken'
|
||||
auth._sessions[tok] = time.time() + 3600
|
||||
|
||||
class _FakeRequest:
|
||||
cookies = {'fam_session': tok}
|
||||
monkeypatch_request = auth.request
|
||||
try:
|
||||
auth.request = _FakeRequest()
|
||||
assert auth.is_authed() is True
|
||||
finally:
|
||||
auth.request = monkeypatch_request
|
||||
|
||||
|
||||
def test_is_authed_false_after_expiry():
|
||||
tok = 'expiredtoken'
|
||||
auth._sessions[tok] = time.time() - 1 # 已过期
|
||||
|
||||
class _FakeRequest:
|
||||
cookies = {'fam_session': tok}
|
||||
monkeypatch_request = auth.request
|
||||
try:
|
||||
auth.request = _FakeRequest()
|
||||
assert auth.is_authed() is False
|
||||
# 过期后应该从 session 表里清掉,不留垃圾
|
||||
assert tok not in auth._sessions
|
||||
finally:
|
||||
auth.request = monkeypatch_request
|
||||
|
||||
|
||||
def test_is_authed_false_without_cookie():
|
||||
class _FakeRequest:
|
||||
cookies = {}
|
||||
monkeypatch_request = auth.request
|
||||
try:
|
||||
auth.request = _FakeRequest()
|
||||
assert auth.is_authed() is False
|
||||
finally:
|
||||
auth.request = monkeypatch_request
|
||||
|
||||
|
||||
def test_whitelist_exact_paths():
|
||||
for path in ('/login', '/api/login', '/api/logout', '/api/auth/check',
|
||||
'/health', '/favicon.ico', '/api/ss/webhook'):
|
||||
assert auth._is_whitelisted(path) is True
|
||||
|
||||
|
||||
def test_whitelist_assets_prefix():
|
||||
assert auth._is_whitelisted('/assets/index-abc123.js') is True
|
||||
|
||||
|
||||
def test_whitelist_rejects_protected_paths():
|
||||
for path in ('/', '/api/ui/people', '/api/chat/ask', '/api/ss/status'):
|
||||
assert auth._is_whitelisted(path) is False
|
||||
Reference in New Issue
Block a user