otter-service-stdalone 0.1.15__py3-none-any.whl → 0.1.18__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- __version__ = "0.1.15"
1
+ __version__ = "0.1.18"
@@ -0,0 +1,62 @@
1
+ import os
2
+ import subprocess
3
+ import yaml
4
+
5
+
6
+ def get(course_key, key_name, sops_path=None, secrets_file=None) -> str:
7
+ """
8
+ This method first tries to decrypt sops file and then tries the
9
+ environment. it will return an Exception if the key is not found
10
+
11
+ :param course_key: the course to find in Google Secrets Manager
12
+ :param key_name: the name of the key to retrieve from the Secrets or the environment
13
+ :param secrets_file: the name of the secrets file associated with the key
14
+ :return: the secret
15
+ """
16
+ try:
17
+ sec = get_via_env(course_key, key_name)
18
+ if sec is None:
19
+ sec = get_via_sops(course_key, key_name, sops_path=sops_path, secrets_file=secrets_file)
20
+ return sec
21
+ except Exception as ex:
22
+ raise Exception(f"Key not decrypted: {key_name}; please configure: Error: {ex}") from ex
23
+
24
+
25
+ def get_via_sops(course_key, key, sops_path=None, secrets_file=None):
26
+ """
27
+ This function attempts to use sops to decrpyt the secrets/{secrets_file}
28
+
29
+ :param course_key: the course to find in Google Secrets Manager
30
+ :param key: the key to find in the course in Google Secrets Manager
31
+ :param sops_path: [OPTIONAL] used to execute pytests
32
+ :param secrets_file: [OPTIONAL] used to execute pytests
33
+ :return: the value of the key or None
34
+ """
35
+ try:
36
+ if sops_path is None:
37
+ sops_path = "/root/go/bin/sops"
38
+
39
+ secrets_file = secrets_file
40
+
41
+ sops_ot = subprocess.check_output([sops_path, "-d", secrets_file], stderr=subprocess.STDOUT)
42
+ dct = yaml.safe_load(sops_ot)
43
+ if course_key is None:
44
+ return dct[key]
45
+ return dct[course_key][key]
46
+ except Exception as ex:
47
+ raise ex
48
+
49
+
50
+ def get_via_env(course_key, key):
51
+ """
52
+ This checks the environment for the key.
53
+
54
+ :param course_key: the course to find in Google Secrets Manager
55
+ :param key: the key to find in the environment
56
+ :return: the value of the key or None
57
+ """
58
+ if key in os.environ:
59
+ if course_key is None:
60
+ return os.environ[key]
61
+ return os.environ[course_key][key]
62
+ return None
@@ -4,158 +4,130 @@ import tornado.web
4
4
  import tornado.auth
5
5
  import os
6
6
  import uuid
7
- from otter_service_stdalone import fs_logging as log, upload_handle as uh
7
+ from otter_service_stdalone import fs_logging as log
8
+ from otter_service_stdalone import user_auth as u_auth
9
+ from otter_service_stdalone import grade_notebooks
8
10
  from zipfile import ZipFile, ZIP_DEFLATED
9
- import asyncio
10
- import async_timeout
11
+
11
12
 
12
13
  __UPLOADS__ = "/tmp/uploads"
14
+ log_debug = f'{os.environ.get("ENVIRONMENT")}-debug'
15
+ log_error = f'{os.environ.get("ENVIRONMENT")}-logs'
16
+
17
+ state = str(uuid.uuid4()) # used to protect against cross-site request forgery attacks.
13
18
 
14
19
 
15
- class GradeNotebooks():
16
- """The class contains the async grade method for executing
17
- otter grader
20
+ class LoginHandler(tornado.web.RequestHandler):
21
+ """Initiaties login auth by authorizing access to github auth api
22
+
23
+ Args:
24
+ tornado (tornado.web.RequestHandler): The request handler
18
25
  """
19
- async def grade(self, p, notebooks_path, results_id):
20
- """Calls otter grade asynchronously and writes the various log files
21
- and results of grading generating by otter-grader
26
+ async def get(self):
27
+ await u_auth.handle_authorization(self, state)
22
28
 
23
- Args:
24
- p (str): the path to autograder.zip -- the solutions
25
- notebooks_path (str): the path to the folder of notebooks to be graded
26
- results_id (str): used for identifying logs
27
29
 
28
- Raises:
29
- Exception: Timeout Exception is raised if async takes longer than 20 min
30
+ class BaseHandler(tornado.web.RequestHandler):
31
+ """This is the super class for the handlers. get_current_user is called by
32
+ any handler that decorated with @tornado.web.authenticated
33
+
34
+ Args:
35
+ tornado (tornado.web.RequestHandler): The request handler
36
+ """
37
+ def get_current_user(self):
38
+ return self.get_secure_cookie("user")
39
+
40
+ def write_error(self, status_code, **kwargs):
41
+ log.write_logs("Http Error", f"{status_code} Error", "", "info", log_error)
42
+ if status_code == 403:
43
+ self.set_status(403)
44
+ self.render("static_templates/403.html")
45
+ else:
46
+ self.set_status(500)
47
+ self.render("static_templates/500.html")
30
48
 
31
- Returns:
32
- boolean: True is the process completes; otherwise an Exception is thrown
33
- """
34
- try:
35
- notebook_folder = uh.handle_upload(notebooks_path, results_id)
36
- log.write_logs(results_id, "Step 5: Notebook Folder configured for grader",
37
- f"Notebook Folder: {notebook_folder}",
38
- "debug",
39
- f'{os.environ.get("ENVIRONMENT")}-debug')
40
- command = [
41
- 'otter', 'grade',
42
- '-a', p,
43
- '-p', notebook_folder,
44
- "--ext", "ipynb",
45
- "--containers", "10",
46
- "-o", notebook_folder,
47
- "-v"
48
- ]
49
- log.write_logs(results_id, f"Step 6: Grading Start: {notebook_folder}",
50
- " ".join(command),
51
- "debug",
52
- f'{os.environ.get("ENVIRONMENT")}-debug')
53
- process = await asyncio.create_subprocess_exec(
54
- *command,
55
- stdin=asyncio.subprocess.PIPE,
56
- stdout=asyncio.subprocess.PIPE,
57
- stderr=asyncio.subprocess.PIPE
58
- )
59
-
60
- # this is waiting for communication back from the process
61
- # some images are quite big and take some time to build the first
62
- # time through - like 20 min for otter-grader
63
- async with async_timeout.timeout(2000):
64
- stdout, stderr = await process.communicate()
65
-
66
- with open(f"{notebook_folder}/grading-output.txt", "w") as f:
67
- for line in stdout.decode().splitlines():
68
- f.write(line + "\n")
69
- log.write_logs(results_id, "Step 7: Grading: Finished: Write: grading-output.txt",
70
- f"{notebook_folder}/grading-output.txt",
71
- "debug",
72
- f'{os.environ.get("ENVIRONMENT")}-debug')
73
- with open(f"{notebook_folder}/grading-logs.txt", "w") as f:
74
- for line in stderr.decode().splitlines():
75
- f.write(line + "\n")
76
- log.write_logs(results_id, "Step 8: Grading: Finished: Write grading-logs.txt",
77
- f"{notebook_folder}/grading-logs.txt",
78
- "debug",
79
- f'{os.environ.get("ENVIRONMENT")}-debug')
80
- log.write_logs(results_id, f"Step 9: Grading: Finished: {notebook_folder}",
81
- " ".join(command),
82
- "debug",
83
- f'{os.environ.get("ENVIRONMENT")}-debug')
84
- log.write_logs(results_id, f"Grading: Finished: {notebook_folder}",
85
- " ".join(command),
86
- "info",
87
- f'{os.environ.get("ENVIRONMENT")}-logs')
88
- return True
89
- except asyncio.TimeoutError:
90
- raise Exception(f'Grading timed out for {notebook_folder}')
91
- except Exception as e:
92
- raise e
93
-
94
-
95
- class Userform(tornado.web.RequestHandler):
96
- """This is the initial landing page for application
49
+
50
+ class GitHubOAuthHandler(BaseHandler):
51
+ """Handles GitHubOAuth
97
52
 
98
53
  Args:
99
54
  tornado (tornado.web.RequestHandler): The request handler
100
55
  """
101
56
  async def get(self):
102
- """renders index.html on a GET HTTP request
103
- """
104
- self.render("index.html", message=None)
57
+ code = self.get_argument('code', False)
58
+ arg_state = self.get_argument('state', False)
59
+ access_token = await u_auth.get_acess_token(arg_state, state, code)
60
+ if access_token:
61
+ user = await u_auth.get_github_username(access_token)
62
+ is_org_member = await u_auth.handle_is_org_member(access_token, user)
63
+ if is_org_member:
64
+ self.set_secure_cookie("user", user, expires_days=7)
65
+ self.redirect("/")
66
+ else:
67
+ raise tornado.web.HTTPError(403)
68
+ else:
69
+ raise tornado.web.HTTPError(500)
105
70
 
106
71
 
107
- class Download(tornado.web.RequestHandler):
72
+ class MainHandler(BaseHandler):
73
+ """This is the initial landing page for application
74
+
75
+ Args:
76
+ BaseHandler (BaseHandler): super class
77
+ """
78
+ @tornado.web.authenticated
79
+ async def get(self):
80
+ self.render("index.html", message=None)
81
+
82
+
83
+ class Download(BaseHandler):
108
84
  """The class handling a request to download results
109
85
 
110
86
  Args:
111
87
  tornado (tornado.web.RequestHandler): The download request handler
112
88
  """
89
+ @tornado.web.authenticated
90
+ def get(self):
91
+ # this just redirects to login and displays main page
92
+ self.render("index.html", message=None)
93
+
94
+ @tornado.web.authenticated
113
95
  async def post(self):
114
96
  """the post method that accepts the code used to locate the results
115
97
  the user wants to download
116
98
  """
117
- code = self.get_argument('download')
118
- directory = f"{__UPLOADS__}/{code}"
119
- if code == "":
120
- log.write_logs(code, "Download: Code Not Given!",
121
- f"{code}",
122
- "debug",
123
- f'{os.environ.get("ENVIRONMENT")}-debug')
99
+ download_code = self.get_argument('download')
100
+ directory = f"{__UPLOADS__}/{download_code}"
101
+ if download_code == "":
102
+ m = "Download: Code Not Given!"
103
+ log.write_logs(download_code, m, f"{download_code}", "debug", log_debug)
124
104
  msg = "Please enter the download code to see your result."
125
105
  self.render("index.html", download_message=msg)
126
106
  elif not os.path.exists(f"{directory}"):
127
- log.write_logs(code, "Download: Directory for Code Not existing",
128
- f"{code}",
129
- "debug",
130
- f'{os.environ.get("ENVIRONMENT")}-debug')
107
+ m = "Download: Directory for Code Not existing"
108
+ log.write_logs(download_code, m, f"{download_code}", "debug", log_debug)
131
109
  msg = "The download code appears to not be correct or expired "
132
- msg += f"- results are deleted regularly: {code}."
110
+ msg += f"- results are deleted regularly: {download_code}."
133
111
  msg += "Please check the code or upload your notebooks "
134
112
  msg += "and autograder.zip for grading again."
135
113
  self.render("index.html", download_message=msg)
136
114
  elif not os.path.exists(f"{directory}/grading-logs.txt"):
137
- log.write_logs(code, "Download: Results Not Ready",
138
- f"{code}",
139
- "debug",
140
- f'{os.environ.get("ENVIRONMENT")}-debug')
115
+ m = "Download: Results Not Ready"
116
+ log.write_logs(download_code, m, f"{download_code}", "debug", log_debug)
141
117
  msg = "The results of your download are not ready yet. "
142
118
  msg += "Please check back."
143
- self.render("index.html", download_message=msg, dcode=code)
119
+ self.render("index.html", download_message=msg, dcode=download_code)
144
120
  else:
145
121
  if not os.path.isfile(f"{directory}/final_grades.csv"):
146
- log.write_logs(code, "Download: final_grades.csv does not exist",
147
- "Problem grading notebooks see stack trace",
148
- "debug",
149
- f'{os.environ.get("ENVIRONMENT")}-debug')
122
+ m = "Download: final_grades.csv does not exist"
123
+ t = "Problem grading notebooks see stack trace"
124
+ log.write_logs(download_code, m, t, "debug", log_debug)
150
125
  with open(f"{directory}/final_grades.csv", "a") as f:
151
126
  m = "There was a problem grading your notebooks. Please see grading-logs.txt"
152
127
  f.write(m)
153
128
  f.close()
154
-
155
- log.write_logs(code, "Download Success: Creating results.zip",
156
- "",
157
- "debug",
158
- f'{os.environ.get("ENVIRONMENT")}-debug')
129
+ m = "Download Success: Creating results.zip"
130
+ log.write_logs(download_code, m, "", "debug", log_debug)
159
131
  with ZipFile(f"{directory}/results.zip", 'w') as zipF:
160
132
  for file in ["final_grades.csv", "grading-logs.txt"]:
161
133
  if os.path.isfile(f"{directory}/{file}"):
@@ -163,7 +135,8 @@ class Download(tornado.web.RequestHandler):
163
135
 
164
136
  self.set_header('Content-Type', 'application/octet-stream')
165
137
  self.set_header("Content-Description", "File Transfer")
166
- self.set_header('Content-Disposition', f"attachment; filename=results-{code}.zip")
138
+ m = f"attachment; filename=results-{download_code}.zip"
139
+ self.set_header('Content-Disposition', m)
167
140
  with open(f"{directory}/results.zip", 'rb') as f:
168
141
  try:
169
142
  while True:
@@ -176,24 +149,27 @@ class Download(tornado.web.RequestHandler):
176
149
  self.write(exc)
177
150
 
178
151
 
179
- class Upload(tornado.web.RequestHandler):
152
+ class Upload(BaseHandler):
180
153
  """This is the upload handler for users to upload autograder.zip and notebooks
181
154
 
182
155
  Args:
183
156
  tornado (tornado.web.RequestHandler): The upload request handler
184
157
  """
158
+ @tornado.web.authenticated
159
+ def get(self):
160
+ # this just redirects to login and displays main page
161
+ self.render("index.html", message=None)
162
+
163
+ @tornado.web.authenticated
185
164
  async def post(self):
186
165
  """this handles the post request and asynchronously launches the grader
187
166
  """
188
- g = GradeNotebooks()
167
+ g = grade_notebooks.GradeNotebooks()
189
168
  files = self.request.files
190
169
  results_path = str(uuid.uuid4())
191
170
  autograder = self.request.files['autograder'][0] if "autograder" in files else None
192
171
  notebooks = self.request.files['notebooks'][0] if "notebooks" in files else None
193
- log.write_logs(results_path, "Step 1: Upload accepted",
194
- "",
195
- "debug",
196
- f'{os.environ.get("ENVIRONMENT")}-debug')
172
+ log.write_logs(results_path, "Step 1: Upload accepted", "", "debug", log_debug)
197
173
  if autograder is not None and notebooks is not None:
198
174
  notebooks_fname = notebooks['filename']
199
175
  notebooks_extn = os.path.splitext(notebooks_fname)[1]
@@ -205,48 +181,42 @@ class Upload(tornado.web.RequestHandler):
205
181
  os.mkdir(__UPLOADS__)
206
182
  auto_p = f"{__UPLOADS__}/{autograder_name}"
207
183
  notebooks_path = f"{__UPLOADS__}/{notebooks_name}"
208
- log.write_logs(results_path, "Step 2a: Uploaded File Names Determined",
209
- f"notebooks path: {notebooks_path}",
210
- "debug",
211
- f'{os.environ.get("ENVIRONMENT")}-debug')
184
+ m = "Step 2a: Uploaded File Names Determined"
185
+ log.write_logs(results_path, m, f"notebooks path: {notebooks_path}", "debug", log_debug)
212
186
  fh = open(auto_p, 'wb')
213
187
  fh.write(autograder['body'])
214
188
 
215
189
  fh = open(notebooks_path, 'wb')
216
190
  fh.write(notebooks['body'])
217
- log.write_logs(results_path, "Step 3: Uploaded Files Written to Disk",
218
- f"Results Code: {results_path}",
219
- "debug",
220
- f'{os.environ.get("ENVIRONMENT")}-debug')
191
+ m = "Step 3: Uploaded Files Written to Disk"
192
+ log.write_logs(results_path, m, f"Results Code: {results_path}", "debug", log_debug)
221
193
  m = "Please save this code. You can retrieve your files by submitting this code "
222
194
  m += f"in the \"Results\" section to the right: {results_path}"
223
195
  self.render("index.html", message=m)
224
196
  try:
225
197
  await g.grade(auto_p, notebooks_path, results_path)
226
198
  except Exception as e:
227
- log.write_logs(results_path, "Grading Problem",
228
- str(e),
229
- "error",
230
- f'{os.environ.get("ENVIRONMENT")}-logs')
199
+ log.write_logs(results_path, "Grading Problem", str(e), "error", log_error)
231
200
  else:
232
- log.write_logs(results_path, "Step 2b: Uploaded Files not given",
233
- "",
234
- "debug",
235
- f'{os.environ.get("ENVIRONMENT")}-debug')
201
+ m = "Step 2b: Uploaded Files not given"
202
+ log.write_logs(results_path, m, "", "debug", log_debug)
236
203
  m = "It looks like you did not set the notebooks or autograder.zip or both!"
237
204
  self.render("index.html", message=m)
238
205
 
239
206
 
240
207
  settings = {
241
208
  "cookie_secret": str(uuid.uuid4()),
242
- "xsrf_cookies": True
209
+ "xsrf_cookies": True,
210
+ "login_url": "/login"
243
211
  }
244
212
 
245
213
  application = tornado.web.Application([
246
- (r"/", Userform),
214
+ (r"/", MainHandler),
215
+ (r"/login", LoginHandler),
247
216
  (r"/upload", Upload),
248
217
  (r"/download", Download),
249
- ], **settings, debug=True)
218
+ (r"/oauth_callback", GitHubOAuthHandler),
219
+ ], **settings, debug=False)
250
220
 
251
221
 
252
222
  def main():
@@ -254,14 +224,11 @@ def main():
254
224
  """
255
225
  try:
256
226
  application.listen(80)
257
- msg = f'{os.environ.get("ENVIRONMENT")}-debug'
258
- log.write_logs("Server Start", "Starting Server", "", "info", msg)
227
+ log.write_logs("Server Start", "Starting Server", "", "info", log_debug)
259
228
  tornado.ioloop.IOLoop.instance().start()
260
229
  except Exception as e:
261
- log.write_logs("Server Start Error", "Server Starting error",
262
- str(e),
263
- "error",
264
- f'{os.environ.get("ENVIRONMENT")}-debug')
230
+ m = "Server Starting error"
231
+ log.write_logs("Server Start Error", m, str(e), "error", log_debug)
265
232
 
266
233
 
267
234
  if __name__ == "__main__":
@@ -50,7 +50,7 @@ def write_logs(id, msg, trace, type, collection):
50
50
  try:
51
51
  db = firestore.client()
52
52
  # this redirects FireStore to local emulator when local testing!
53
- if os.getenv("ENVIRONMENT") == "otter-stdalone-docker-local-test":
53
+ if "local" in os.getenv("ENVIRONMENT"):
54
54
  channel = grpc.insecure_channel("host.docker.internal:8080")
55
55
  transport = firestore_grpc_transport.FirestoreGrpcTransport(channel=channel)
56
56
  db._firestore_api_internal = firestore_client.FirestoreClient(transport=transport)
@@ -0,0 +1,103 @@
1
+ import asyncio
2
+ import async_timeout
3
+ from otter_service_stdalone import fs_logging as log, upload_handle as uh
4
+ import os
5
+
6
+ log_debug = f'{os.environ.get("ENVIRONMENT")}-debug'
7
+ log_count = f'{os.environ.get("ENVIRONMENT")}-count'
8
+ log_error = f'{os.environ.get("ENVIRONMENT")}-logs'
9
+
10
+
11
+ class GradeNotebooks():
12
+ """The class contains the async grade method for executing
13
+ otter grader as well as a function for logging the number of
14
+ notebooks to be graded
15
+ """
16
+
17
+ def count_ipynb_files(self, directory, extension):
18
+ """this count the files for logging purposes"""
19
+ count = 0
20
+ for filename in os.listdir(directory):
21
+ if filename.endswith(extension):
22
+ count += 1
23
+ return count
24
+
25
+ async def grade(self, p, notebooks_path, results_id):
26
+ """Calls otter grade asynchronously and writes the various log files
27
+ and results of grading generating by otter-grader
28
+
29
+ Args:
30
+ p (str): the path to autograder.zip -- the solutions
31
+ notebooks_path (str): the path to the folder of notebooks to be graded
32
+ results_id (str): used for identifying logs
33
+
34
+ Raises:
35
+ Exception: Timeout Exception is raised if async takes longer than 20 min
36
+
37
+ Returns:
38
+ boolean: True is the process completes; otherwise an Exception is thrown
39
+ """
40
+ try:
41
+ notebook_folder = uh.handle_upload(notebooks_path, results_id)
42
+ notebook_count = self.count_ipynb_files(notebook_folder, ".ipynb")
43
+ log.write_logs(results_id, f"{notebook_count}",
44
+ "",
45
+ "info",
46
+ f'{os.environ.get("ENVIRONMENT")}-count')
47
+ log.write_logs(results_id, "Step 5: Notebook Folder configured for grader",
48
+ f"Notebook Folder: {notebook_folder}",
49
+ "debug",
50
+ log_debug)
51
+ command = [
52
+ 'otter', 'grade',
53
+ '-a', p,
54
+ '-p', notebook_folder,
55
+ "--ext", "ipynb",
56
+ "--containers", "10",
57
+ "-o", notebook_folder,
58
+ "-v"
59
+ ]
60
+ log.write_logs(results_id, f"Step 6: Grading Start: {notebook_folder}",
61
+ " ".join(command),
62
+ "debug",
63
+ log_debug)
64
+ process = await asyncio.create_subprocess_exec(
65
+ *command,
66
+ stdin=asyncio.subprocess.PIPE,
67
+ stdout=asyncio.subprocess.PIPE,
68
+ stderr=asyncio.subprocess.PIPE
69
+ )
70
+
71
+ # this is waiting for communication back from the process
72
+ # some images are quite big and take some time to build the first
73
+ # time through - like 20 min for otter-grader
74
+ async with async_timeout.timeout(2000):
75
+ stdout, stderr = await process.communicate()
76
+
77
+ with open(f"{notebook_folder}/grading-output.txt", "w") as f:
78
+ for line in stdout.decode().splitlines():
79
+ f.write(line + "\n")
80
+ log.write_logs(results_id, "Step 7: Grading: Finished: Write: grading-output.txt",
81
+ f"{notebook_folder}/grading-output.txt",
82
+ "debug",
83
+ log_debug)
84
+ with open(f"{notebook_folder}/grading-logs.txt", "w") as f:
85
+ for line in stderr.decode().splitlines():
86
+ f.write(line + "\n")
87
+ log.write_logs(results_id, "Step 8: Grading: Finished: Write grading-logs.txt",
88
+ f"{notebook_folder}/grading-logs.txt",
89
+ "debug",
90
+ log_debug)
91
+ log.write_logs(results_id, f"Step 9: Grading: Finished: {notebook_folder}",
92
+ " ".join(command),
93
+ "debug",
94
+ log_debug)
95
+ log.write_logs(results_id, f"Grading: Finished: {notebook_folder}",
96
+ " ".join(command),
97
+ "info",
98
+ log_error)
99
+ return True
100
+ except asyncio.TimeoutError:
101
+ raise Exception(f'Grading timed out for {notebook_folder}')
102
+ except Exception as e:
103
+ raise e
@@ -0,0 +1,17 @@
1
+ github_access_id: ENC[AES256_GCM,data:L97KjBroMsmVUYBOwIhGirB2nns=,iv:ZcBj74anvs43HziVBeQQRwl7hoEtX36+2wEQbFPNS0c=,tag:ptO+Tsx37xk1pWAGjctuhQ==,type:str]
2
+ github_access_secret: ENC[AES256_GCM,data:5DuzOLRI8HwW5KhskZQW+HzwPiIHBlYcBCMpfAt34dZg9lOB+ivrcw==,iv:Y4H6MrJT1dnO1HsOy6XYDQfUERXCssu+7/FP5SEEnCw=,tag:UYp6KrHWG4XrxY8LroSO6Q==,type:str]
3
+ sops:
4
+ kms: []
5
+ gcp_kms:
6
+ - resource_id: projects/ucb-datahub-2018/locations/global/keyRings/datahub/cryptoKeys/sops
7
+ created_at: "2024-04-03T18:29:45Z"
8
+ enc: CiUA67O9AArlsf5j4XCvPmf4qs3txe7+SxbVuJggNgW3GLh2DdACEkkAYHATHigxI73bJa6VfxZDxj5KLXsnz0ql72q7r7dzgSCwwjtFRrqizyloq2JvIoJskpjBcZBIe4VVVXHXsg8TUX/iMEB2G0Wm
9
+ azure_kv: []
10
+ hc_vault: []
11
+ age: []
12
+ lastmodified: "2024-04-03T18:29:46Z"
13
+ mac: ENC[AES256_GCM,data:QWZetePfWsqQQtCKc6O++f1falvJujzYMhxqRQFPmoUmQrFHBZ06v4XPPUciZGcrSgkhuyQwgHYXXSap1rDba2/D2i8jy8n8jC+hop60HLsveY53+ahYu3Fs91iQh3UdYg5ecTE0I/6prCdqVNL8nshYSBILYVNVG4KJAI7+G4s=,iv:4Beqcu1nYExqiv58Cs/YlCFGS1UV/MkAC5SWdmHr9Jw=,tag:h5PXnYsDtTFBFqkfGpPaJw==,type:str]
14
+ pgp: []
15
+ unencrypted_suffix: _unencrypted
16
+ version: 3.7.1
17
+
@@ -0,0 +1,16 @@
1
+ github_access_id: ENC[AES256_GCM,data:MULsaNCCrl+bF/HvLO00HKfCw54=,iv:Wi4txCUYoM+Zi3CHTSjqSuGAwUToZdggDOVuaZndHLA=,tag:CtKnanwlrdXLnPs74ZsqOQ==,type:str]
2
+ github_access_secret: ENC[AES256_GCM,data:h/cKH4nRjylg8ELxXsTvRuEuuB0P87R6SLqrbEBkQrjjgT+Bv/SVhQ==,iv:WmFvG6nijf9u0NE3ykJDaVxLdhbnDeiXbnexlYJDClE=,tag:HSO2CzLpCiqJJ9UdLn3+aw==,type:str]
3
+ sops:
4
+ kms: []
5
+ gcp_kms:
6
+ - resource_id: projects/ucb-datahub-2018/locations/global/keyRings/datahub/cryptoKeys/sops
7
+ created_at: "2024-03-26T22:42:25Z"
8
+ enc: CiUA67O9AORb6b4pqLYE7IAfXT1a6ZHA9TycUT/TmUecInMxiq8aEkkAYHATHnnEuC/W1e4eckSfEz+yQe5Eu7CrX7X8f8DTBIFLLyu6tFgSKjr3VxitXNmBhOY5rQ53e+M/QbgjeJ2LexKjuWCFck+A
9
+ azure_kv: []
10
+ hc_vault: []
11
+ age: []
12
+ lastmodified: "2024-03-26T22:42:26Z"
13
+ mac: ENC[AES256_GCM,data:n0QV+da9B/zogfbFCjY94Sr+uaTZBUjyjIvqOh7nIf/OCTjLwN6dsJlF6QCLlWIChTS+hfPn+8gEmJpn7gTrJIYfzO77ByP2hIxu+CAjLGHJ4V3/QrY3odPPzbiRv6KXpVpCt/JWXGcoimKMeFStwtaUDDWqh/xDbb7UwciXFxE=,iv:GAML0oOeBpI2SUW3NkNNJbWlhGBP4AyjUlZVG+qnds0=,tag:+1xj3mMmL33pBSWT6NyAjw==,type:str]
14
+ pgp: []
15
+ unencrypted_suffix: _unencrypted
16
+ version: 3.7.1
@@ -0,0 +1,16 @@
1
+ github_access_id: ENC[AES256_GCM,data:c8f28uHdSyPTkYqGeYQLTVilBfM=,iv:xF06JH1aACwzifY6dLpGyNC/Gp4UfUrRHhHEKRq86RQ=,tag:crN2FEg5rcam9XlWO26Bag==,type:str]
2
+ github_access_secret: ENC[AES256_GCM,data:paO5S9N9cn4/fiyG6P16550hL/r7dz8EHRMJLTaPdZgBtrHnaMq6HQ==,iv:qPAEDBcRJYyq0tPj7vPDPd+x1zhEhaTZm7TTOW3JNyU=,tag:9lKfrBt1Fnaq9T5J8jy4Hw==,type:str]
3
+ sops:
4
+ kms: []
5
+ gcp_kms:
6
+ - resource_id: projects/ucb-datahub-2018/locations/global/keyRings/datahub/cryptoKeys/sops
7
+ created_at: "2024-04-03T18:24:30Z"
8
+ enc: CiUA67O9AKNwFJ3SWLlDu7J9GHFv6BLZeu15atOKMwR5Z1pXtaPEEkkAYHATHmUfzxyp7nC6ly2ICTtW6rd7b9qG4w2N2n4rlC+0/eT0MjTQIDB4WBa65YGfp+8dYQXS6S6sBTK+ozbX6nXtcinddCcr
9
+ azure_kv: []
10
+ hc_vault: []
11
+ age: []
12
+ lastmodified: "2024-04-03T18:24:31Z"
13
+ mac: ENC[AES256_GCM,data:kCdn15/6xjxE3YYJMNJJaLmP7gswC2gEPop+ptkyq+TPu1ufG6kGaCaDbXScr6w7Qe/RXnzdeOdTp/nF7nSRL9YRH6acR2RXcWuV6ngWFUM+YJXgiM6uDB1Ps+yn2OdzEa1v8LZo0gGRxraHdScqTxJ0bL7koFQBKiJf7CWdhD8=,iv:VoFzGQLkufxBFZVtGMxK9x2jF2jIpVPvjdWB8Z3Edo4=,tag:KCix8V1a7V9vCbt4A7kxtA==,type:str]
14
+ pgp: []
15
+ unencrypted_suffix: _unencrypted
16
+ version: 3.7.1
@@ -0,0 +1,16 @@
1
+ github_access_id: ENC[AES256_GCM,data:3R9AMcMccaaF+1z3zuu2aehmJHI=,iv:mnhhWTTXsbAJN0FdoFzpc8WrImYLSM1chk3lqwxFSkw=,tag:RSBoplL8Ve7MjTW6Zz7WMQ==,type:str]
2
+ github_access_secret: ENC[AES256_GCM,data:+ebdQ0Jxy/bRe5plcuZZIrDoULu02jE+vj2HxoERYDG462YRONtiww==,iv:fHLqi4D1ixCdoOK5NASkkVLdX7kExnh8nOIOPaksnN0=,tag:9Jl2lJajSzO4UzJ/T+A8sw==,type:str]
3
+ sops:
4
+ kms: []
5
+ gcp_kms:
6
+ - resource_id: projects/ucb-datahub-2018/locations/global/keyRings/datahub/cryptoKeys/sops
7
+ created_at: "2024-04-03T18:27:16Z"
8
+ enc: CiUA67O9AKF7k4zHdbhRYNzT1cKTzkS1by8G/1516HbJQBlOsnf2EkkAYHATHos+I2G4tn7GIRJhjrsXLeN++ZUUyICCn3VB1Wzcm2udHOfce4GGGc0XcQc18Nvn1Zr/ToroSv9qI8DarocbEHZX+H/l
9
+ azure_kv: []
10
+ hc_vault: []
11
+ age: []
12
+ lastmodified: "2024-04-03T18:27:17Z"
13
+ mac: ENC[AES256_GCM,data:Q/A3QkCxo1GdcSB/gtAyB3DFaXKrcAtpzXpe6GJYD8jD1v5N7rAW/DFo459r5jfAM+9W4Ks3cwTheBJgJnqxfHRuWp6FcP6GefNuScGsiRequp/FQwJcTMnAgLOGCsOGbSfzzsNVEm7j0VpkTkgavr/rFrqAlVC1MFT++bJsXTw=,iv:P86phogwEJL0ZMkBWH+qJWS8MyHbI1/0iM399cyuLxQ=,tag:MdO0PHXoe893Mk0GTfms+g==,type:str]
14
+ pgp: []
15
+ unencrypted_suffix: _unencrypted
16
+ version: 3.7.1
@@ -0,0 +1,24 @@
1
+ <html>
2
+ <head>
3
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
4
+ <title>403 Forbidden</title>
5
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">
6
+ <script defer src="https://use.fontawesome.com/releases/v5.3.1/js/all.js"></script>
7
+ </head>
8
+ <body>
9
+ <section class="container">
10
+ <div class="hero is-info welcome is-small">
11
+ <div class="hero-body">
12
+ <div class="container">
13
+ <h1 class="title">403 Forbidden</h1>
14
+ <p class="subtitle colored is-3">
15
+ You do not have permission to access this resource or your authentication needs to be renewed.
16
+ Please <a style='color:#005b96' href="/login">login</a> to try again or
17
+ email sean.smorris@berkeley.edu for help.
18
+ </p>
19
+ </div>
20
+ </div>
21
+ </div>
22
+ </section>
23
+ </body>
24
+ </html>
@@ -0,0 +1,24 @@
1
+ <html>
2
+ <head>
3
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
4
+ <title>Access Problem</title>
5
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">
6
+ <script defer src="https://use.fontawesome.com/releases/v5.3.1/js/all.js"></script>
7
+ </head>
8
+ <body>
9
+ <section class="container">
10
+ <div class="hero is-info welcome is-small">
11
+ <div class="hero-body">
12
+ <div class="container">
13
+ <h1 class="title">500 Forbidden - Access Problem</h1>
14
+ <p class="subtitle colored is-3">
15
+ We are unable to establish access for you.
16
+ Please email: sean.smorris@berkeley.edu or try
17
+ <a style='color:#005b96' href="/login">logging</a> in again.
18
+ </p>
19
+ </div>
20
+ </div>
21
+ </div>
22
+ </section>
23
+ </body>
24
+ </html>
@@ -0,0 +1,120 @@
1
+ import json
2
+ import os
3
+ from otter_service_stdalone import fs_logging as log, access_sops_keys
4
+ import requests
5
+ import tornado
6
+ import urllib.parse
7
+
8
+
9
+ log_coll = f'{os.environ.get("ENVIRONMENT")}-debug'
10
+ environment_name = os.environ.get("ENVIRONMENT").split("-")[-1]
11
+ gh_key_path = os.path.join(os.path.dirname(__file__), f"secrets/gh_key.{environment_name}.yaml")
12
+ github_id = access_sops_keys.get(None, "github_access_id", secrets_file=gh_key_path)
13
+ github_secret = access_sops_keys.get(None, "github_access_secret", secrets_file=gh_key_path)
14
+
15
+
16
+ async def handle_authorization(form, state):
17
+ """authorizes via oauth app token access github auth api
18
+
19
+ Parameters:
20
+ - form (tornado.web.RequestHandler): The request handler used to re-direct for
21
+ authorization. The redirect url is configured at the github endpoint for the app
22
+ - state (int): random uuid4 number generated to ensure communication between endpoints is
23
+ not compromised
24
+ """
25
+ log.write_logs("Auth Workflow", "UserAuth: Get: Authorizing", "", "info", log_coll)
26
+ q_params = f"client_id={github_id}&state={state}&scope=read:org"
27
+ form.redirect(f'https://github.com/login/oauth/authorize?{q_params}')
28
+
29
+
30
+ async def get_acess_token(arg_state, state, code):
31
+ """requests and returns the access token or None
32
+
33
+ Parameters:
34
+ - arg_state (int): the state argument being passed on the url string; this will be compared
35
+ to the state value generated in this file to ensure they are the same!
36
+ - state (int): random uuid4 number generated to ensure communication between endpoints is
37
+ not compromised
38
+ - code (str): the code that is returned from the authorization request
39
+
40
+ Returns:
41
+ - str: the access token used for subsequent calls to the api; or None
42
+ """
43
+ http_client = tornado.httpclient.AsyncHTTPClient()
44
+ params = {
45
+ 'client_id': github_id,
46
+ 'client_secret': github_secret,
47
+ 'code': code,
48
+ 'redirect_uri': f"{os.environ.get('GRADER_DNS')}/oauth_callback"
49
+ }
50
+ m = "UserAuth: GitHubOAuthHandler: Getting Access Token"
51
+ log.write_logs("Auth Workflow", m, "", "info", log_coll)
52
+ response = await http_client.fetch(
53
+ 'https://github.com/login/oauth/access_token',
54
+ method='POST',
55
+ headers={'Accept': 'application/json'},
56
+ body=urllib.parse.urlencode(params)
57
+ )
58
+ resp = json.loads(response.body.decode())
59
+ access_token = resp['access_token']
60
+ if arg_state != state:
61
+ access_token = None
62
+ m = "UserAuth: GitHubOAuthHandler: Cross-Site Forgery possible - aborting"
63
+ log.write_logs("Auth Workflow", m, "", "info", log_coll)
64
+ else:
65
+ m = "UserAuth: GitHubOAuthHandler: Access Token Granted"
66
+ log.write_logs("Auth Workflow", m, "", "info", log_coll)
67
+ return access_token
68
+
69
+
70
+ async def handle_is_org_member(access_token, user):
71
+ """the final authorization is to make sure the member has access to the application by being
72
+ a part of the correct organizaton
73
+
74
+ Parameters:
75
+ - access_token (str): The OAuth access token.
76
+ - user (str): the authenticated GitHub user name
77
+
78
+ Returns:
79
+ - boolean: True user is in the GH org, False otherwise
80
+ """
81
+ log.write_logs("Auth Workflow", "UserAuth: Get: Check Membership", "", "info", log_coll)
82
+ if user:
83
+ org_name = os.environ.get("AUTH_ORG")
84
+ url = f'https://api.github.com/orgs/{org_name}/members/{user}'
85
+ headers = {
86
+ 'Authorization': f'token {access_token}',
87
+ 'Accept': 'application/vnd.github.v3+json',
88
+ }
89
+ response = requests.get(url, headers=headers)
90
+ return response.status_code == 204
91
+ else:
92
+ return False
93
+
94
+
95
+ async def get_github_username(access_token):
96
+ """
97
+ Retrieve the GitHub username of the authenticated user.
98
+
99
+ Parameters:
100
+ - access_token (str): The OAuth access token.
101
+
102
+ Returns:
103
+ - str: The username of the authenticated user, or None if not found.
104
+ """
105
+ url = 'https://api.github.com/user'
106
+ headers = {
107
+ 'Authorization': f'token {access_token}',
108
+ 'Accept': 'application/vnd.github.v3+json',
109
+ }
110
+
111
+ response = requests.get(url, headers=headers)
112
+ if response.status_code == 200:
113
+ m = "UserAuth: Get: UserName - Success"
114
+ log.write_logs("Auth Workflow", m, "", "info", log_coll)
115
+ user_info = response.json()
116
+ return user_info.get('login')
117
+ else:
118
+ m = f"UserAuth: Get: UserName - Fail:{response.status_code}"
119
+ log.write_logs("Auth Workflow", m, "", "info", log_coll)
120
+ return None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
- Name: otter-service-stdalone
3
- Version: 0.1.15
2
+ Name: otter_service_stdalone
3
+ Version: 0.1.18
4
4
  Summary: Grading Service for Instructors using Otter Grader
5
5
  Home-page: https://github.com/sean-morris/otter-service-stdalone
6
6
  Author: Sean Morris
@@ -0,0 +1,19 @@
1
+ otter_service_stdalone/__init__.py,sha256=6BiuMUkhwQp6bzUZSF8np8F1NwCltEtK0sPBF__tepU,23
2
+ otter_service_stdalone/access_sops_keys.py,sha256=nboU5aZ84Elrm5vO0dMgpIF5LLcnecpNAwpxKvj6DvU,2129
3
+ otter_service_stdalone/app.py,sha256=JrjykDRx1VlxLwZQ_svsM9Pabwr5H8x6AHTI-LUJ_BA,9462
4
+ otter_service_stdalone/fs_logging.py,sha256=IKFZkc5TmpI6G3vTYFAU9jDjQ-GT5aRxk8kdZ0h0kJE,2390
5
+ otter_service_stdalone/grade_notebooks.py,sha256=DiIW7oIwq2TROHFiR0A9qqVCoFzd3QhZXxqgWZtCDk8,4498
6
+ otter_service_stdalone/index.html,sha256=QbSQs31OZhWlCQFE5vvJOlNh-JHEJ3PZPgR4GukzrCA,6032
7
+ otter_service_stdalone/upload_handle.py,sha256=NB6isuLrLkUCPetUA3ugUlKKpAYw4nvDBVmxpzvgcE8,5157
8
+ otter_service_stdalone/user_auth.py,sha256=O25EekGSnodWlO6RFftK0_QObbJ4MOQupGJZ4XOeHb0,4616
9
+ otter_service_stdalone/secrets/gh_key.dev.yaml,sha256=XIIjkGhzA16bJGM3imwL1K6_9xw6-5DuiZ-e73LYA_8,1128
10
+ otter_service_stdalone/secrets/gh_key.local.yaml,sha256=xMp-IkvntbaDLdao1iDkLPGHrx9-X29t4kC3GnXq03U,1127
11
+ otter_service_stdalone/secrets/gh_key.prod.yaml,sha256=auPYkkWa3y2p0vEbDG4ZVrah78o-8d9PIjoDF3E6qlI,1127
12
+ otter_service_stdalone/secrets/gh_key.staging.yaml,sha256=uBgz5UIjxT5XBioDchQQ3X22A1UTZC7mZ_mXbEDTRjM,1127
13
+ otter_service_stdalone/static_templates/403.html,sha256=K7Jc85v6K8ieRtzBo3JBX__oJCcZ8MEiZcIrhITD5I0,985
14
+ otter_service_stdalone/static_templates/500.html,sha256=2D0YF8-ocJ9I-hM3P3KdflhggKH52L4bTHhKzd79B9o,944
15
+ otter_service_stdalone-0.1.18.dist-info/METADATA,sha256=7YDBLYbK4CDJxVKHjayAePGzn_Ncmsm2Ewck-3RrkDI,1345
16
+ otter_service_stdalone-0.1.18.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
17
+ otter_service_stdalone-0.1.18.dist-info/entry_points.txt,sha256=cx447chuIEl8ly9jEoF5-2xNhaKsWcIMDdhUMH_00qQ,75
18
+ otter_service_stdalone-0.1.18.dist-info/top_level.txt,sha256=6UP22fD4OhbLt23E01v8Kvjn44vPRbyTIg_GqMYL-Ng,23
19
+ otter_service_stdalone-0.1.18.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.41.3)
2
+ Generator: bdist_wheel (0.43.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,10 +0,0 @@
1
- otter_service_stdalone/__init__.py,sha256=qb0TalpSt1CbprnFyeLUKqgrqNtmnk9IoQQ7umAoXVY,23
2
- otter_service_stdalone/app.py,sha256=0pDM8cA5Ay9JNZkLFuL7PRNepV855g7W7oCKYW46tes,11748
3
- otter_service_stdalone/fs_logging.py,sha256=LTrgadyjQyZ204sS5B1HtchnBV49SKCpfrJdcfuvApo,2417
4
- otter_service_stdalone/index.html,sha256=QbSQs31OZhWlCQFE5vvJOlNh-JHEJ3PZPgR4GukzrCA,6032
5
- otter_service_stdalone/upload_handle.py,sha256=NB6isuLrLkUCPetUA3ugUlKKpAYw4nvDBVmxpzvgcE8,5157
6
- otter_service_stdalone-0.1.15.dist-info/METADATA,sha256=C5eFfMoR2WQoFiwXvqgkDacdGZN1cVolJotGKsNe9sI,1345
7
- otter_service_stdalone-0.1.15.dist-info/WHEEL,sha256=Xo9-1PvkuimrydujYJAjF7pCkriuXBpUPEjma1nZyJ0,92
8
- otter_service_stdalone-0.1.15.dist-info/entry_points.txt,sha256=cx447chuIEl8ly9jEoF5-2xNhaKsWcIMDdhUMH_00qQ,75
9
- otter_service_stdalone-0.1.15.dist-info/top_level.txt,sha256=6UP22fD4OhbLt23E01v8Kvjn44vPRbyTIg_GqMYL-Ng,23
10
- otter_service_stdalone-0.1.15.dist-info/RECORD,,