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