users.py 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. """The users bundle: the user-management page, roles and the profile
  2. page (the roles decision: normal users manage projects, tasks, goals and
  3. rules; admins additionally manage users, saddles and capabilities)."""
  4. from harness import * # noqa: F401,F403
  5. def run(ctx):
  6. admin = ctx.auth
  7. check(True, "login yields an auth slot")
  8. print("== users page: admin CRUD + newest-first ==")
  9. users_state = slot_public(admin, "/users", "users")
  10. check(users_state["create"] is not None, "admin sees the create action")
  11. seeded = [u for u in users_state["users"] if u["username"] == "admin"]
  12. check(len(seeded) == 1 and seeded[0]["is_admin"] and seeded[0]["is_me"],
  13. "seeded admin row carries the admin badge and is_me")
  14. result = act(admin, users_state["create"], {
  15. "username": "bob", "email": "bob@example.com",
  16. "forename": "Bob", "surname": "Builder",
  17. "date_of_birth": "1990-05-05",
  18. "password": "bobpass1", "confirm_password": "bobpass1"})
  19. check("created" in (action_notify(result) or ""), "create bob")
  20. result = act(admin, users_state["create"], {
  21. "username": "carol", "email": "carol@example.com",
  22. "forename": "Carol", "surname": "Coda",
  23. "date_of_birth": "1985-03-03",
  24. "password": "carolpass", "confirm_password": "carolpass",
  25. "admin": "1"})
  26. check("created" in (action_notify(result) or ""), "create carol as admin")
  27. users_state = slot_public(admin, "/users", "users")
  28. order = [u["username"] for u in users_state["users"]]
  29. check(order == ["carol", "bob", "admin"], "users table is newest-first: %s" % order)
  30. bob = [u for u in users_state["users"] if u["username"] == "bob"][0]
  31. carol = [u for u in users_state["users"] if u["username"] == "carol"][0]
  32. me_row = [u for u in users_state["users"] if u["is_me"]][0]
  33. check(bob["is_admin"] is False, "bob has the user badge")
  34. check(carol["is_admin"] is True, "carol has the admin badge")
  35. # The serialiser drops null properties: a row carries exactly one
  36. # of grant/revoke, whichever applies to its current role.
  37. check(bob.get("grant_admin") is not None and "revoke_admin" not in bob,
  38. "non-admin row carries grant (not revoke)")
  39. check(carol.get("revoke_admin") is not None and "grant_admin" not in carol,
  40. "admin row carries revoke (not grant)")
  41. check(me_row["revoke_admin"] is not None, "own row carries revoke (server refuses it)")
  42. print("== roles: non-admin refused on /users and the admin mutations ==")
  43. bob_auth = login("bob", "bobpass1")
  44. frames = directives("/users", bob_auth)
  45. navs = [d.get("uri") for d in frames if d.get("type") == "navigate"]
  46. check(navs == ["/login"], "non-admin /users entrypoint navigates to /login")
  47. # The saddles page itself stays OPEN to the non-admin; only its
  48. # mutations are admin-gated (the roles decision).
  49. saddles_state = slot_public(bob_auth, "/saddles", "saddles")
  50. check(saddles_state["is_admin"] is False, "saddles page open, controls hidden for non-admin")
  51. result = act(bob_auth, saddles_state["register"], {"name": "x", "host": "h:1"})
  52. check(any(d.get("type") == "navigate" for d in result), "non-admin register saddle refused")
  53. result = act(bob_auth, saddles_state["create_capability"], {"name": "nope"})
  54. check(any(d.get("type") == "navigate" for d in result), "non-admin create capability refused")
  55. # The admin path stays green after the gates.
  56. saddles_state = slot_public(admin, "/saddles", "saddles")
  57. result = act(admin, saddles_state["create_capability"], {"name": "gpu"})
  58. check("created" in (action_notify(result) or ""), "admin creates a capability")
  59. print("== self-protection guards ==")
  60. result = act(admin, me_row["delete"], {})
  61. check("own account" in (action_notify(result) or ""), "self-delete refused")
  62. result = act(admin, me_row["set_enabled"], {})
  63. check("own account" in (action_notify(result) or ""), "self-disable refused")
  64. result = act(admin, me_row["revoke_admin"], {})
  65. check("own admin" in (action_notify(result) or ""), "self-demote refused")
  66. result = act(admin, me_row["alter"], {
  67. "username": "admin", "email": "admin@example.com",
  68. "forename": "Admin", "surname": "User",
  69. "date_of_birth": "1980-01-01", "enabled": "1"})
  70. check("own account" in (action_notify(result) or ""), "self-edit refused (belongs on /profile)")
  71. print("== admin edits another user: rename + reset password ==")
  72. result = act(admin, bob["alter"], {
  73. "username": "bob", "email": "bob2@example.com",
  74. "forename": "Bob", "surname": "B",
  75. "date_of_birth": "1990-05-05", "enabled": "1",
  76. "new_password": "bobnewpass"})
  77. check("saved" in (action_notify(result) or ""), "edit bob (rename + reset password)")
  78. login("bob", "bobnewpass")
  79. check(True, "bob logs in with the reset password")
  80. # disable: the session dies on the next guarded request and the
  81. # login is refused.
  82. users_state = slot_public(admin, "/users", "users")
  83. bob = [u for u in users_state["users"] if u["username"] == "bob"][0]
  84. result = act(admin, bob["set_enabled"], {})
  85. check("disabled" in (action_notify(result) or ""), "disable bob")
  86. frames = directives("/projects", bob_auth)
  87. navs = [d.get("uri") for d in frames if d.get("type") == "navigate"]
  88. check(navs == ["/login"], "disabled bob's entrypoints bounce to /login")
  89. result = act(admin, bob["delete"], {})
  90. check("deleted" in (action_notify(result) or ""), "delete bob")
  91. print("== role toggles on another user ==")
  92. result = act(admin, carol["revoke_admin"], {})
  93. check("revoked" in (action_notify(result) or ""), "demote carol (admin remains)")
  94. users_state = slot_public(admin, "/users", "users")
  95. carol = [u for u in users_state["users"] if u["username"] == "carol"][0]
  96. check(carol["is_admin"] is False, "carol's badge flips to user")
  97. result = act(admin, carol["grant_admin"], {})
  98. check("granted" in (action_notify(result) or ""), "re-grant carol admin")
  99. print("== profile: every user changes their own password ==")
  100. profile = slot_public(admin, "/profile", "profile")
  101. check(profile["username"] == "admin" and profile["is_admin"] is True,
  102. "profile shows the own row with the role")
  103. result = act(admin, profile["change_password"], {
  104. "current_password": "WRONG", "new_password": "newpass9",
  105. "confirm_password": "newpass9"})
  106. check("incorrect" in (action_notify(result) or ""), "wrong current password refused")
  107. result = act(admin, profile["change_password"], {
  108. "current_password": "adminpass123", "new_password": "newpass9",
  109. "confirm_password": "newpass9"})
  110. check("changed" in (action_notify(result) or ""), "password changed")
  111. # Spry's change-own-password revokes the user's auth slots: the
  112. # seeded handle above dies with the old password, so re-login and
  113. # use the fresh slot for everything after this point.
  114. admin = login("admin", "newpass9")
  115. check(True, "admin logs in with the new password")
  116. carol_auth = login("carol", "carolpass")
  117. carol_profile = slot_public(carol_auth, "/profile", "profile")
  118. check(carol_profile["username"] == "carol", "carol's profile shows her row")
  119. result = act(carol_auth, carol_profile["change_password"], {
  120. "current_password": "carolpass", "new_password": "carolnew1",
  121. "confirm_password": "carolnew1"})
  122. check("changed" in (action_notify(result) or ""), "carol changes her password")
  123. login("carol", "carolnew1")
  124. check(True, "carol logs in with the new password")
  125. # The seeded admin password has changed mid-bundle; later login
  126. # helpers (if any re-login) use the new one.
  127. print("== cleanup: single admin remains ==")
  128. users_state = slot_public(admin, "/users", "users")
  129. carol = [u for u in users_state["users"] if u["username"] == "carol"][0]
  130. result = act(admin, carol["revoke_admin"], {})
  131. check("revoked" in (action_notify(result) or ""), "demote carol before delete")
  132. result = act(admin, carol["delete"], {})
  133. check("deleted" in (action_notify(result) or ""), "delete carol")
  134. check([u["username"] for u in slot_public(admin, "/users", "users")["users"]] == ["admin"],
  135. "the admin remains the sole user")