test_admin_api_auth.py 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. from __future__ import annotations
  2. from datetime import UTC, datetime, timedelta
  3. from types import SimpleNamespace
  4. from aiohttp import CookieJar
  5. from aiohttp.test_utils import TestClient, TestServer
  6. from pyrogram.enums import ChatMemberStatus
  7. async def test_login_forced_password_change_csrf_and_session(app_modules):
  8. admin_api = app_modules.load("wbb.admin.api")
  9. application = admin_api.build_admin_application()
  10. await application["admin_api"].initialize()
  11. client = TestClient(TestServer(application), cookie_jar=CookieJar(unsafe=True))
  12. await client.start_server()
  13. try:
  14. invalid = await client.post(
  15. "/api/admin/v1/auth/login",
  16. json={"username": "admin", "password": "wrong"},
  17. )
  18. assert invalid.status == 401
  19. assert (await invalid.json())["error"]["code"] == "invalid_credentials"
  20. login = await client.post(
  21. "/api/admin/v1/auth/login",
  22. json={"username": "admin", "password": "qwe0.123456"},
  23. )
  24. assert login.status == 200
  25. login_data = (await login.json())["data"]
  26. assert login_data["must_change_password"] is True
  27. blocked = await client.get("/api/admin/v1/dashboard")
  28. assert blocked.status == 428
  29. assert (await blocked.json())["error"]["code"] == "password_change_required"
  30. no_csrf = await client.put(
  31. "/api/admin/v1/auth/password",
  32. json={
  33. "current_password": "qwe0.123456",
  34. "new_password": "changed-pass-123",
  35. },
  36. )
  37. assert no_csrf.status == 403
  38. changed = await client.put(
  39. "/api/admin/v1/auth/password",
  40. headers={"X-CSRF-Token": login_data["csrf_token"]},
  41. json={
  42. "current_password": "qwe0.123456",
  43. "new_password": "changed-pass-123",
  44. },
  45. )
  46. assert changed.status == 200
  47. changed_data = (await changed.json())["data"]
  48. assert changed_data["must_change_password"] is False
  49. unconfirmed = await client.patch(
  50. "/api/admin/v1/chats/-100/profile",
  51. headers={"X-CSRF-Token": changed_data["csrf_token"]},
  52. json={"title": "New title"},
  53. )
  54. assert unconfirmed.status == 409
  55. assert (await unconfirmed.json())["error"]["code"] == "confirmation_required"
  56. dashboard = await client.get("/api/admin/v1/dashboard")
  57. assert dashboard.status == 200
  58. assert "counts" in (await dashboard.json())["data"]
  59. giveaway_db = app_modules.load("wbb.utils.dbgiveaway")
  60. now = datetime.now(UTC)
  61. giveaway = await giveaway_db.create_giveaway(
  62. chat_id=-100,
  63. creator_id=1,
  64. creator_name="Admin",
  65. title="Original",
  66. description="",
  67. prizes=[{"name": "Winner", "count": 1}],
  68. starts_at=now + timedelta(hours=1),
  69. ends_at=now + timedelta(hours=2),
  70. )
  71. await giveaway_db.attach_giveaway_message(
  72. giveaway["giveaway_id"], -100, 88
  73. )
  74. giveaway = await giveaway_db.get_giveaway(giveaway["giveaway_id"])
  75. app_modules.app.members[(-100, 999)] = SimpleNamespace(
  76. status=ChatMemberStatus.OWNER
  77. )
  78. expected_updated_at = giveaway["updated_at"]
  79. if expected_updated_at.tzinfo is None:
  80. expected_updated_at = expected_updated_at.replace(tzinfo=UTC)
  81. update_body = {
  82. "title": "Updated",
  83. "description": "Valid for one month",
  84. "starts_at": (now + timedelta(hours=1)).isoformat(),
  85. "ends_at": (now + timedelta(hours=3)).isoformat(),
  86. "expected_updated_at": expected_updated_at.isoformat(),
  87. "confirm": True,
  88. }
  89. update_without_csrf = await client.patch(
  90. f"/api/admin/v1/giveaways/{giveaway['giveaway_id']}",
  91. json=update_body,
  92. )
  93. assert update_without_csrf.status == 403
  94. update_without_confirmation = await client.patch(
  95. f"/api/admin/v1/giveaways/{giveaway['giveaway_id']}",
  96. headers={"X-CSRF-Token": changed_data["csrf_token"]},
  97. json={**update_body, "confirm": False},
  98. )
  99. assert update_without_confirmation.status == 409
  100. updated = await client.patch(
  101. f"/api/admin/v1/giveaways/{giveaway['giveaway_id']}",
  102. headers={"X-CSRF-Token": changed_data["csrf_token"]},
  103. json=update_body,
  104. )
  105. assert updated.status == 200
  106. updated_data = (await updated.json())["data"]
  107. assert updated_data["title"] == "Updated"
  108. assert updated_data["description"] == "Valid for one month"
  109. assert app_modules.app.edited_messages[-1][:2] == (-100, 88)
  110. audit = await app_modules.wbb.db.admin_audit_logs.find_one(
  111. {"action": "giveaway.update", "target_id": giveaway["giveaway_id"]}
  112. )
  113. assert audit["success"] is True
  114. await app_modules.wbb.db.managed_chats.insert_one(
  115. {
  116. "bot_id": "primary",
  117. "chat_id": -100,
  118. "title": "机器人已退出的群",
  119. "accessible": False,
  120. }
  121. )
  122. delete_without_csrf = await client.delete("/api/admin/v1/chats/-100")
  123. assert delete_without_csrf.status == 403
  124. deleted = await client.delete(
  125. "/api/admin/v1/chats/-100",
  126. headers={"X-CSRF-Token": changed_data["csrf_token"]},
  127. )
  128. assert deleted.status == 200
  129. assert (await deleted.json())["data"] == {
  130. "chat_id": "-100",
  131. "removed": True,
  132. }
  133. assert await app_modules.wbb.db.managed_chats.find_one({"chat_id": -100}) is None
  134. logout = await client.post(
  135. "/api/admin/v1/auth/logout",
  136. headers={"X-CSRF-Token": changed_data["csrf_token"]},
  137. )
  138. assert logout.status == 200
  139. unauthenticated = await client.get("/api/admin/v1/dashboard")
  140. assert unauthenticated.status == 401
  141. finally:
  142. await client.close()
  143. async def test_login_is_rate_limited_with_mongo_naive_datetime(app_modules):
  144. admin_api = app_modules.load("wbb.admin.api")
  145. application = admin_api.build_admin_application()
  146. await application["admin_api"].initialize()
  147. client = TestClient(TestServer(application), cookie_jar=CookieJar(unsafe=True))
  148. await client.start_server()
  149. try:
  150. for _ in range(5):
  151. response = await client.post(
  152. "/api/admin/v1/auth/login",
  153. json={"username": "admin", "password": "wrong"},
  154. )
  155. assert response.status == 401
  156. await app_modules.wbb.db.admin_users.update_one(
  157. {"username": "admin"},
  158. {"$set": {"last_failed_login_at": datetime.now(UTC).replace(tzinfo=None)}},
  159. )
  160. limited = await client.post(
  161. "/api/admin/v1/auth/login",
  162. json={"username": "admin", "password": "qwe0.123456"},
  163. )
  164. assert limited.status == 429
  165. assert (await limited.json())["error"]["code"] == "login_rate_limited"
  166. expired_at = datetime.now(UTC) - timedelta(minutes=16)
  167. await app_modules.wbb.db.admin_users.update_one(
  168. {"username": "admin"},
  169. {"$set": {"last_failed_login_at": expired_at.replace(tzinfo=None)}},
  170. )
  171. login = await client.post(
  172. "/api/admin/v1/auth/login",
  173. json={"username": "admin", "password": "qwe0.123456"},
  174. )
  175. assert login.status == 200
  176. finally:
  177. await client.close()