test_auth.py 5.3 KB

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