File size: 5,477 Bytes
979853c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 | import importlib
import sys
from types import SimpleNamespace
import bcrypt
import pytest
from lightrag.api.passwords import BCRYPT_PASSWORD_PREFIX, hash_password
from lightrag.tools.hash_password import main as hash_password_main
from lightrag.utils import logger as lightrag_logger
def import_real_api_module(module_name: str):
sys.modules.pop(module_name, None)
package_name, _, child_name = module_name.rpartition(".")
package = sys.modules.get(package_name)
if package is not None and hasattr(package, child_name):
delattr(package, child_name)
return importlib.import_module(module_name)
@pytest.fixture
def auth_module(monkeypatch):
config = import_real_api_module("lightrag.api.config")
mock_global_args = SimpleNamespace(
token_secret="test-jwt-secret",
jwt_algorithm="HS256",
token_expire_hours=48,
guest_token_expire_hours=24,
auth_accounts="admin:admin_pass",
)
monkeypatch.setattr(config, "global_args", mock_global_args)
module = import_real_api_module("lightrag.api.auth")
module = importlib.reload(module)
yield module
sys.modules.pop("lightrag.api.auth", None)
def build_bcrypt_value(password: str) -> str:
hashed = bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()).decode("utf-8")
return f"{BCRYPT_PASSWORD_PREFIX}{hashed}"
def test_verify_plaintext_password(auth_module):
handler = auth_module.AuthHandler()
handler.accounts = {"admin": "admin_pass"}
assert handler.verify_password("admin", "admin_pass")
assert not handler.verify_password("admin", "wrong_pass")
def test_verify_prefixed_bcrypt_password(auth_module):
handler = auth_module.AuthHandler()
handler.accounts = {"user": build_bcrypt_value("user_pass")}
assert handler.verify_password("user", "user_pass")
assert not handler.verify_password("user", "wrong_pass")
def test_plaintext_password_with_bcrypt_prefix_stays_plaintext(auth_module):
handler = auth_module.AuthHandler()
handler.accounts = {"user": "$2b$not-a-real-hash"}
assert handler.verify_password("user", "$2b$not-a-real-hash")
assert not handler.verify_password("user", "anything-else")
def test_invalid_auth_accounts_raises(monkeypatch):
config = import_real_api_module("lightrag.api.config")
mock_global_args = SimpleNamespace(
token_secret="test-jwt-secret",
jwt_algorithm="HS256",
token_expire_hours=48,
guest_token_expire_hours=24,
auth_accounts="admin",
)
monkeypatch.setattr(config, "global_args", mock_global_args)
with pytest.raises(ValueError, match="AUTH_ACCOUNTS must use"):
import_real_api_module("lightrag.api.auth")
sys.modules.pop("lightrag.api.auth", None)
def test_initialize_config_rejects_default_token_secret_with_auth_accounts():
config = import_real_api_module("lightrag.api.config")
insecure_args = SimpleNamespace(
auth_accounts="admin:admin_pass",
token_secret=config.DEFAULT_TOKEN_SECRET,
)
with pytest.raises(ValueError, match="TOKEN_SECRET must be explicitly set"):
config.initialize_config(insecure_args, force=True)
def test_initialize_config_allows_custom_token_secret_with_auth_accounts():
config = import_real_api_module("lightrag.api.config")
secure_args = SimpleNamespace(
auth_accounts="admin:admin_pass",
token_secret="custom-jwt-secret",
)
initialized = config.initialize_config(secure_args, force=True)
assert initialized is secure_args
def test_guest_tokens_fall_back_to_default_secret_when_token_secret_missing(
monkeypatch,
):
config = import_real_api_module("lightrag.api.config")
mock_global_args = SimpleNamespace(
token_secret=None,
jwt_algorithm="HS256",
token_expire_hours=48,
guest_token_expire_hours=24,
auth_accounts="",
)
monkeypatch.setattr(config, "global_args", mock_global_args)
warning_messages = []
def capture_warning(message):
warning_messages.append(message)
monkeypatch.setattr(lightrag_logger, "warning", capture_warning)
module = import_real_api_module("lightrag.api.auth")
module = importlib.reload(module)
handler = module.AuthHandler()
token = handler.create_token("guest", role="guest")
token_info = handler.validate_token(token)
assert handler.secret == config.DEFAULT_TOKEN_SECRET
assert token_info["username"] == "guest"
assert token_info["role"] == "guest"
assert any(
"Falling back to the default guest-mode JWT secret" in msg
for msg in warning_messages
)
sys.modules.pop("lightrag.api.auth", None)
def test_hash_password_returns_prefixed_value(auth_module):
hashed = hash_password("new_password")
assert hashed.startswith(BCRYPT_PASSWORD_PREFIX)
raw_hash = hashed[len(BCRYPT_PASSWORD_PREFIX) :]
assert bcrypt.checkpw("new_password".encode("utf-8"), raw_hash.encode("utf-8"))
def test_hash_password_cli_outputs_auth_accounts_entry(capsys):
exit_code = hash_password_main(["--username", "admin", "secret"])
assert exit_code == 0
output = capsys.readouterr().out.strip()
username, hashed = output.split(":", 1)
assert username == "admin"
assert hashed.startswith(BCRYPT_PASSWORD_PREFIX)
raw_hash = hashed[len(BCRYPT_PASSWORD_PREFIX) :]
assert bcrypt.checkpw("secret".encode("utf-8"), raw_hash.encode("utf-8"))
|