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.
 
 
 

248 lines
8.6 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. User authentication for normal interactions
  18. """
  19. import os
  20. import logging
  21. import importlib
  22. from . import api
  23. from . import loader
  24. from . import cslog
  25. from . import base_context
  26. importlib.reload(base_context)
  27. LOGGER = logging.getLogger("cs")
  28. _nodoc = {"LOGGER"}
  29. def _execfile(*args):
  30. fn = args[0]
  31. with open(fn) as f:
  32. c = compile(f.read(), fn, "exec")
  33. exec(c, *args[1:])
  34. def get_auth_type(context):
  35. """
  36. Return the methods associated with the currently chosen authentication
  37. type.
  38. **Parameters**:
  39. * `context`: the context associated with this request.
  40. **Returns:** a dictionary containing the variables defined in the
  41. authentication type specified by `context['cs_auth_type']`.
  42. """
  43. auth_type = context["cs_auth_type"]
  44. return get_auth_type_by_name(context, auth_type)
  45. def get_auth_type_by_name(context, auth_type):
  46. """
  47. Helper function. Returns the methods associated with the given
  48. authentication type, regardless of which is active.
  49. **Parameters**:
  50. * `context`: the context associated with this request.
  51. * `auth_type`: the name of the authentication type in question.
  52. **Returns:** a dictionary containing the variables defined in the
  53. authentication type specified by `auth_type`.
  54. """
  55. fs_root = context.get("cs_fs_root", base_context.cs_fs_root)
  56. data_root = context.get("cs_data_root", base_context.cs_data_root)
  57. course = context["cs_course"]
  58. tail = os.path.join("__AUTH__", auth_type, "%s.py" % auth_type)
  59. course_loc = os.path.join(data_root, "courses", course, tail)
  60. global_loc = os.path.join(fs_root, tail)
  61. e = dict(context)
  62. # look in course, then global; error if not found
  63. if course is not None and os.path.isfile(course_loc):
  64. _execfile(course_loc, e)
  65. elif os.path.isfile(global_loc):
  66. _execfile(global_loc, e)
  67. else:
  68. # no valid auth type found
  69. raise Exception("Invalid cs_auth_type: %s" % auth_type)
  70. return e
  71. def generate_api_token_for_user(context, user_info):
  72. """
  73. Generate an API token for a given user (specified by the user_info dict), if needed.
  74. Add the api token to the dict, if a new one is generated.
  75. """
  76. if "username" in user_info:
  77. # successful login. check for existing token
  78. tok = cslog.most_recent("_api_users", [], user_info["username"], None)
  79. if tok is None:
  80. # if no token found, create a new one.
  81. tok = api.initialize_api_token(context, user_info)
  82. LOGGER.info("[auth] Initializing new API token for %s" % user_info)
  83. user_info["api_token"] = tok
  84. def get_logged_in_user(context):
  85. """
  86. From the context, get information about the logged in user.
  87. If the context has an API token in it, that value will be used to determine
  88. who is logged in. Otherwise, the currently-active authentication type's
  89. `get_logged_in_user` function is used.
  90. Side effect: if the user in question does not have an API token already, a
  91. new one is created for them.
  92. **Parameters:**
  93. * `context`: the context associated with this request
  94. **Returns:** a dictionary containing the information associated with the
  95. user who is currently logged in. If a user is logged in, it will contain
  96. the key `api_token`, as well as some subset of the keys `'username'`,
  97. `'name'`, and `'email'`.
  98. """
  99. # handle auto-login for LTI users
  100. lti_data = context["cs_session_data"].get("lti_data")
  101. doing_logout = context["cs_form"].get("loginaction", None) == "logout"
  102. if lti_data and not doing_logout:
  103. cui = lti_data.get("cs_user_info")
  104. context["cs_user_info"] = cui
  105. generate_api_token_for_user(context, cui)
  106. LOGGER.info("[auth] Allowing in LTI user with cs_user_info=%s" % cui)
  107. return cui
  108. # if an API token was specified, use the associated information and move on
  109. # this has the side-effect of renewing that token (moving back the
  110. # expiration time)
  111. api_user = api.get_logged_in_user(context)
  112. if api_user is not None:
  113. return api_user
  114. regular_user = get_auth_type(context)["get_logged_in_user"](context)
  115. generate_api_token_for_user(context, regular_user)
  116. return regular_user
  117. def get_user_information(context):
  118. """
  119. Based on the context, load extra information about the user who is logged
  120. in.
  121. This method is used to load any information specified about the user in a
  122. course's `__USERS__` directory, or from a global log. For example,
  123. course-level permissions are loaded this way.
  124. This function is also responsible for handling impersonation of other
  125. users.
  126. **Parameters:**
  127. * `context`: the context associated with this request
  128. **Returns:** a dictionary like that returned by `get_logged_in_user`, but
  129. (possibly) with additional mappings as specified in the loaded file.
  130. """
  131. return _get_user_information(
  132. context,
  133. context["cs_user_info"],
  134. context.get("cs_course", None),
  135. context["cs_username"],
  136. )
  137. def _get_user_information(context, into, course, username, do_preload=False):
  138. if course is not None:
  139. if do_preload:
  140. loader.load_global_data(context)
  141. loader.do_preload(context, course, [], context)
  142. fname = os.path.join( # path to user definition py file in course data
  143. context["cs_data_root"],
  144. "courses",
  145. context["cs_course"],
  146. "__USERS__",
  147. "%s.py" % username,
  148. )
  149. else:
  150. fname = os.path.join(context["cs_data_root"], "_logs", username)
  151. if os.path.exists(fname):
  152. with open(fname) as f:
  153. text = f.read()
  154. exec(text, into)
  155. loader.clean_builtins(into)
  156. LOGGER.warning("[auth] loaded from %s user=%s" % (fname, into))
  157. else:
  158. LOGGER.error("[auth] missing user definition file %s" % fname)
  159. # permissions handling
  160. if "permissions" not in into:
  161. if "role" not in into:
  162. into["role"] = context.get("cs_default_role", None)
  163. plist = context.get("cs_permissions", {})
  164. defaults = context.get("cs_default_permissions", {"view"})
  165. into["permissions"] = set(plist.get(into["role"], defaults))
  166. spoofed_role = context.get("cs_form", {}).get("as_role", None)
  167. if spoofed_role is not None and "impersonate" in into["permissions"]:
  168. into["role"] = spoofed_role
  169. orig_p = into["permissions"]
  170. spoofed_p = plist.get(spoofed_role, defaults)
  171. new_p = set(spoofed_p).intersection(set(orig_p))
  172. for i in ("submit_all", "view_all"):
  173. lesser = i.split("_")[0]
  174. if i in orig_p and i not in new_p and lesser in spoofed_p:
  175. new_p.add(lesser)
  176. into["permissions"] = new_p
  177. # impersonation
  178. if ("as" in context.get("cs_form", {})) and ("real_user" not in into):
  179. if "impersonate" not in into["permissions"]:
  180. return into
  181. old = dict(into)
  182. old["p"] = into["permissions"]
  183. context["cs_username"] = context["cs_form"]["as"]
  184. into["real_user"] = old
  185. into["username"] = into["name"] = context["cs_username"]
  186. into["role"] = None
  187. into["permissions"] = []
  188. into["api_token"] = old["api_token"]
  189. into = get_user_information(context)
  190. cslog = context["csm_cslog"]
  191. if "username" in into:
  192. logininfo = cslog.most_recent("_logininfo", [], into["username"], {})
  193. logininfo = {
  194. k: v for k, v in logininfo.items() if k in ("username", "name", "email")
  195. }
  196. into.update(logininfo)
  197. extra_info = cslog.most_recent("_extra_info", [], into["username"], {})
  198. into.update(extra_info)
  199. if str(username) == "None":
  200. if "view" in into["permissions"]:
  201. into["permissions"] = ["view"]
  202. else:
  203. into["permissions"] = []
  204. return into