CAT-SOOP is a flexible, programmable learning management system based on the Python programming language. https://catsoop.mit.edu
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

202 lines
7.1 KiB

  1. # This file is part of CAT-SOOP
  2. # Copyright (c) 2011-2020 by The CAT-SOOP Developers <catsoop-dev@mit.edu>
  3. #
  4. # This program is free software: you can redistribute it and/or modify it under
  5. # the terms of the GNU Affero General Public License as published by the Free
  6. # Software Foundation, either version 3 of the License, or (at your option) any
  7. # later version.
  8. #
  9. # This program is distributed in the hope that it will be useful, but WITHOUT
  10. # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
  11. # FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
  12. # details.
  13. #
  14. # You should have received a copy of the GNU Affero General Public License
  15. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  16. """
  17. Methods related to authentication for API access
  18. """
  19. import os
  20. import uuid
  21. import random
  22. import string
  23. _nodoc = {"CHARACTERS"}
  24. CHARACTERS = string.ascii_letters + string.digits
  25. def new_api_token(context, username):
  26. """
  27. Generate a new API token for the given user.
  28. **Parameters**:
  29. * `context`: the context associated with this request
  30. * `username`: the username for which a new API token should be generated
  31. **Returns:** a new API token for the given user, with length as given by
  32. `cs_api_token_length`.
  33. """
  34. length = context.get("cs_api_token_length", 70)
  35. seed = username + uuid.uuid4().hex
  36. r = random.Random()
  37. r.seed(seed)
  38. return "".join(r.choice(CHARACTERS) for i in range(length))
  39. def initialize_api_token(context, user_info):
  40. """
  41. Intialize an API token for a user, and store the association in the
  42. database.
  43. **Parameters:**
  44. * `context`: the context associated with this request
  45. * `user_info`: the user_info dictionary associated with this request
  46. (should ideally contain `'username'`, `'name'`, and `'email'` keys).
  47. **Returns:** the newly-generated API token
  48. """
  49. user_info = {
  50. k: v for (k, v) in user_info.items() if k in {"username", "name", "email"}
  51. }
  52. token = new_api_token(context, user_info["username"])
  53. context["csm_cslog"].overwrite_log("_api_tokens", [], str(token), user_info)
  54. context["csm_cslog"].update_log("_api_users", [], user_info["username"], token)
  55. return token
  56. def userinfo_from_token(context, tok):
  57. """
  58. Given an API token, return the associated user's information.
  59. **Parameters:**
  60. * `context`: the context associated with this request
  61. * `tok`: an API token
  62. **Returns:** a dictionary containing the information associated with the
  63. user who holds the given API token; it will contain some subset of the keys
  64. `'username'`, `'name'`, and `'email'`. Returns `None` if the given token
  65. is invalid.
  66. """
  67. return context["csm_cslog"].most_recent("_api_tokens", [], str(tok), None)
  68. def get_logged_in_user(context):
  69. """
  70. Helper function. Given a request context, returns the information
  71. associated with the user making the request if an API token is present.
  72. **Parameters:**
  73. * `context`: the context associated with this request
  74. **Returns:** a dictionary containing the information associated with the
  75. user who holds the given API token; it will contain the key `api_token`, as
  76. well as some subset of the keys `'username'`, `'name'`, and `'email'`.
  77. """
  78. form = context.get("cs_form", {})
  79. if "api_token" in form:
  80. tok = form["api_token"]
  81. log = userinfo_from_token(context, tok)
  82. if log is not None:
  83. log["api_token"] = tok
  84. return log
  85. return None
  86. def get_user_information(
  87. context, uname=None, passwd=None, api_token=None, course=None, _as=None
  88. ):
  89. """
  90. Return the information associated with a user identified by an API token or
  91. a username and password.
  92. **Parameters:**
  93. * `context`: the context associated with this request
  94. **Optional Parameters:**
  95. * `uname`: if logging in via password, this is the username of the user in
  96. question.
  97. * `passwd`: if logging in via password, this is the hex-encoded 32-bit
  98. result of hashing the user's password with pbkdf2 for 100000 iterations,
  99. with a salt given by the username and the password concatenated
  100. together. this is not the user's password in plain text.
  101. * `api_token`: if logging in via API token, this is the token of the person
  102. making the request.
  103. * `course`: if given, relevant user information from that course (section,
  104. role, permissions, etc) will be included in the result
  105. * `_as`: if making this request to get someone else's information, this is
  106. the other person's username. must have relevant permissions.
  107. **Returns:** a dictionary with two keys. `'ok'` maps to a Boolean
  108. indicating whether the lookup was successful.
  109. * If `'ok`' maps to `False`, then the additional key is `'error'`, which
  110. maps to a string containing an error message.
  111. * If `'ok`' maps to `True`, then the additional key is `'user_info'`, which
  112. maps to a dictionary containing the user's information.
  113. """
  114. login = context["csm_auth"].get_auth_type_by_name(context, "login")
  115. user = None
  116. error = None
  117. log = context["csm_cslog"]
  118. if api_token is not None:
  119. # if there is an API token, check it.
  120. user = userinfo_from_token(context, api_token)
  121. if user is None:
  122. error = "Invalid API token: %s" % api_token
  123. else:
  124. user["api_token"] = api_token
  125. extra_info = log.most_recent("_extra_info", [], user["username"], {})
  126. user.update(extra_info)
  127. else:
  128. if uname is not None and passwd is not None:
  129. # if no API token was given, but username and password were, check
  130. # those.
  131. hash_iters = context.get("cs_password_hash_iterations", 500000)
  132. pwd_check = login.check_password(context, passwd, uname, hash_iters)
  133. if not pwd_check:
  134. error = "Invalid username or password."
  135. else:
  136. user = log.most_recent("_logininfo", [], user["username"], None)
  137. else:
  138. error = "API token or username and password hash required."
  139. if user is None and error is None:
  140. # catch-all error: if we haven't authenticated but don't have an error
  141. # messge, use this one.
  142. error = "Could not authenticate"
  143. if error is None and course is not None:
  144. # if we have successfully logged in and a course is specified, we need to
  145. # look up extra information from the course in question.
  146. ctx = context["csm_loader"].generate_context([course])
  147. ctx["cs_form"] = {}
  148. if _as is not None:
  149. ctx["cs_form"]["as"] = _as
  150. base_loc = os.path.join(context["cs_data_root"], "courses", course)
  151. if os.path.isdir(base_loc):
  152. uname = user["username"]
  153. ctx["cs_user_info"] = user
  154. user = context["csm_auth"]._get_user_information(
  155. ctx, user, course, uname, do_preload=True
  156. )
  157. else:
  158. error = "No such course: %s" % course
  159. if error is not None:
  160. return {"ok": False, "error": error}
  161. else:
  162. return {"ok": True, "user_info": user}