| 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")) |
|
|