otter-service-stdalone 0.1.16__py3-none-any.whl → 0.1.19__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.16"
1
+ __version__ = "0.1.19"
@@ -14,10 +14,10 @@ def get(course_key, key_name, sops_path=None, secrets_file=None) -> str:
14
14
  :return: the secret
15
15
  """
16
16
  try:
17
- secret = get_via_env(course_key, key_name)
18
- if secret is None:
19
- secret = get_via_sops(course_key, key_name, sops_path=sops_path, secrets_file=secrets_file)
20
- return secret
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
21
  except Exception as ex:
22
22
  raise Exception(f"Key not decrypted: {key_name}; please configure: Error: {ex}") from ex
23
23
 
@@ -38,8 +38,8 @@ def get_via_sops(course_key, key, sops_path=None, secrets_file=None):
38
38
 
39
39
  secrets_file = secrets_file
40
40
 
41
- sops_output = subprocess.check_output([sops_path, "-d", secrets_file], stderr=subprocess.STDOUT)
42
- dct = yaml.safe_load(sops_output)
41
+ sops_ot = subprocess.check_output([sops_path, "-d", secrets_file], stderr=subprocess.STDOUT)
42
+ dct = yaml.safe_load(sops_ot)
43
43
  if course_key is None:
44
44
  return dct[key]
45
45
  return dct[course_key][key]
@@ -4,185 +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
- import urllib.parse
12
- from otter_service_stdalone import access_sops_keys
13
- import json
11
+
14
12
 
15
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.
16
18
 
17
19
 
18
- class GradeNotebooks():
19
- """The class contains the async grade method for executing
20
- 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
21
25
  """
22
- async def grade(self, p, notebooks_path, results_id):
23
- """Calls otter grade asynchronously and writes the various log files
24
- and results of grading generating by otter-grader
26
+ async def get(self):
27
+ await u_auth.handle_authorization(self, state)
25
28
 
26
- Args:
27
- p (str): the path to autograder.zip -- the solutions
28
- notebooks_path (str): the path to the folder of notebooks to be graded
29
- results_id (str): used for identifying logs
30
29
 
31
- Raises:
32
- 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")
33
48
 
34
- Returns:
35
- boolean: True is the process completes; otherwise an Exception is thrown
36
- """
37
- try:
38
- notebook_folder = uh.handle_upload(notebooks_path, results_id)
39
- log.write_logs(results_id, "Step 5: Notebook Folder configured for grader",
40
- f"Notebook Folder: {notebook_folder}",
41
- "debug",
42
- f'{os.environ.get("ENVIRONMENT")}-debug')
43
- command = [
44
- 'otter', 'grade',
45
- '-a', p,
46
- '-p', notebook_folder,
47
- "--ext", "ipynb",
48
- "--containers", "10",
49
- "-o", notebook_folder,
50
- "-v"
51
- ]
52
- log.write_logs(results_id, f"Step 6: Grading Start: {notebook_folder}",
53
- " ".join(command),
54
- "debug",
55
- f'{os.environ.get("ENVIRONMENT")}-debug')
56
- process = await asyncio.create_subprocess_exec(
57
- *command,
58
- stdin=asyncio.subprocess.PIPE,
59
- stdout=asyncio.subprocess.PIPE,
60
- stderr=asyncio.subprocess.PIPE
61
- )
62
-
63
- # this is waiting for communication back from the process
64
- # some images are quite big and take some time to build the first
65
- # time through - like 20 min for otter-grader
66
- async with async_timeout.timeout(2000):
67
- stdout, stderr = await process.communicate()
68
-
69
- with open(f"{notebook_folder}/grading-output.txt", "w") as f:
70
- for line in stdout.decode().splitlines():
71
- f.write(line + "\n")
72
- log.write_logs(results_id, "Step 7: Grading: Finished: Write: grading-output.txt",
73
- f"{notebook_folder}/grading-output.txt",
74
- "debug",
75
- f'{os.environ.get("ENVIRONMENT")}-debug')
76
- with open(f"{notebook_folder}/grading-logs.txt", "w") as f:
77
- for line in stderr.decode().splitlines():
78
- f.write(line + "\n")
79
- log.write_logs(results_id, "Step 8: Grading: Finished: Write grading-logs.txt",
80
- f"{notebook_folder}/grading-logs.txt",
81
- "debug",
82
- f'{os.environ.get("ENVIRONMENT")}-debug')
83
- log.write_logs(results_id, f"Step 9: Grading: Finished: {notebook_folder}",
84
- " ".join(command),
85
- "debug",
86
- f'{os.environ.get("ENVIRONMENT")}-debug')
87
- log.write_logs(results_id, f"Grading: Finished: {notebook_folder}",
88
- " ".join(command),
89
- "info",
90
- f'{os.environ.get("ENVIRONMENT")}-logs')
91
- return True
92
- except asyncio.TimeoutError:
93
- raise Exception(f'Grading timed out for {notebook_folder}')
94
- except Exception as e:
95
- raise e
96
-
97
-
98
- class Userform(tornado.web.RequestHandler):
99
- """This is the initial landing page for application
49
+
50
+ class GitHubOAuthHandler(BaseHandler):
51
+ """Handles GitHubOAuth
100
52
 
101
53
  Args:
102
54
  tornado (tornado.web.RequestHandler): The request handler
103
55
  """
104
56
  async def get(self):
105
- # User will be redirected here by GitHub after authorization
106
57
  code = self.get_argument('code', False)
107
- if code:
108
- access_token = await self.get_authenticated_user(code)
109
- self.write(f'Your GitHub access token is: {access_token}')
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)
110
68
  else:
111
- self.render("index.html", message=None)
112
-
113
- async def get_authenticated_user(self, code):
114
- secrets_file = os.path.join(os.path.dirname(__file__), "secrets/gh_key.yaml")
115
- github_id = access_sops_keys.get(None, "github_access_id", secrets_file=secrets_file)
116
- github_secret = access_sops_keys.get(None, "github_access_secret", secrets_file=secrets_file)
117
-
118
- http_client = tornado.httpclient.AsyncHTTPClient()
119
- params = {
120
- 'client_id': github_id,
121
- 'client_secret': github_secret,
122
- 'code': code,
123
- 'redirect_uri': "https://grader.datahub.berkeley.edu/oauth_callback"
124
- }
125
- response = await http_client.fetch(
126
- 'https://github.com/login/oauth/access_token',
127
- method='POST',
128
- headers={'Accept': 'application/json'},
129
- body=urllib.parse.urlencode(params)
130
- )
131
- return json.loads(response.body.decode())['access_token']
132
-
133
-
134
- class Download(tornado.web.RequestHandler):
69
+ raise tornado.web.HTTPError(500)
70
+
71
+
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):
135
84
  """The class handling a request to download results
136
85
 
137
86
  Args:
138
87
  tornado (tornado.web.RequestHandler): The download request handler
139
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
140
95
  async def post(self):
141
96
  """the post method that accepts the code used to locate the results
142
97
  the user wants to download
143
98
  """
144
- code = self.get_argument('download')
145
- directory = f"{__UPLOADS__}/{code}"
146
- if code == "":
147
- log.write_logs(code, "Download: Code Not Given!",
148
- f"{code}",
149
- "debug",
150
- 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)
151
104
  msg = "Please enter the download code to see your result."
152
105
  self.render("index.html", download_message=msg)
153
106
  elif not os.path.exists(f"{directory}"):
154
- log.write_logs(code, "Download: Directory for Code Not existing",
155
- f"{code}",
156
- "debug",
157
- 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)
158
109
  msg = "The download code appears to not be correct or expired "
159
- msg += f"- results are deleted regularly: {code}."
110
+ msg += f"- results are deleted regularly: {download_code}."
160
111
  msg += "Please check the code or upload your notebooks "
161
112
  msg += "and autograder.zip for grading again."
162
113
  self.render("index.html", download_message=msg)
163
114
  elif not os.path.exists(f"{directory}/grading-logs.txt"):
164
- log.write_logs(code, "Download: Results Not Ready",
165
- f"{code}",
166
- "debug",
167
- 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)
168
117
  msg = "The results of your download are not ready yet. "
169
118
  msg += "Please check back."
170
- self.render("index.html", download_message=msg, dcode=code)
119
+ self.render("index.html", download_message=msg, dcode=download_code)
171
120
  else:
172
121
  if not os.path.isfile(f"{directory}/final_grades.csv"):
173
- log.write_logs(code, "Download: final_grades.csv does not exist",
174
- "Problem grading notebooks see stack trace",
175
- "debug",
176
- 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)
177
125
  with open(f"{directory}/final_grades.csv", "a") as f:
178
126
  m = "There was a problem grading your notebooks. Please see grading-logs.txt"
179
127
  f.write(m)
180
128
  f.close()
181
-
182
- log.write_logs(code, "Download Success: Creating results.zip",
183
- "",
184
- "debug",
185
- f'{os.environ.get("ENVIRONMENT")}-debug')
129
+ m = "Download Success: Creating results.zip"
130
+ log.write_logs(download_code, m, "", "debug", log_debug)
186
131
  with ZipFile(f"{directory}/results.zip", 'w') as zipF:
187
132
  for file in ["final_grades.csv", "grading-logs.txt"]:
188
133
  if os.path.isfile(f"{directory}/{file}"):
@@ -190,7 +135,8 @@ class Download(tornado.web.RequestHandler):
190
135
 
191
136
  self.set_header('Content-Type', 'application/octet-stream')
192
137
  self.set_header("Content-Description", "File Transfer")
193
- 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)
194
140
  with open(f"{directory}/results.zip", 'rb') as f:
195
141
  try:
196
142
  while True:
@@ -203,24 +149,27 @@ class Download(tornado.web.RequestHandler):
203
149
  self.write(exc)
204
150
 
205
151
 
206
- class Upload(tornado.web.RequestHandler):
152
+ class Upload(BaseHandler):
207
153
  """This is the upload handler for users to upload autograder.zip and notebooks
208
154
 
209
155
  Args:
210
156
  tornado (tornado.web.RequestHandler): The upload request handler
211
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
212
164
  async def post(self):
213
165
  """this handles the post request and asynchronously launches the grader
214
166
  """
215
- g = GradeNotebooks()
167
+ g = grade_notebooks.GradeNotebooks()
216
168
  files = self.request.files
217
169
  results_path = str(uuid.uuid4())
218
170
  autograder = self.request.files['autograder'][0] if "autograder" in files else None
219
171
  notebooks = self.request.files['notebooks'][0] if "notebooks" in files else None
220
- log.write_logs(results_path, "Step 1: Upload accepted",
221
- "",
222
- "debug",
223
- f'{os.environ.get("ENVIRONMENT")}-debug')
172
+ log.write_logs(results_path, "Step 1: Upload accepted", "", "debug", log_debug)
224
173
  if autograder is not None and notebooks is not None:
225
174
  notebooks_fname = notebooks['filename']
226
175
  notebooks_extn = os.path.splitext(notebooks_fname)[1]
@@ -232,49 +181,42 @@ class Upload(tornado.web.RequestHandler):
232
181
  os.mkdir(__UPLOADS__)
233
182
  auto_p = f"{__UPLOADS__}/{autograder_name}"
234
183
  notebooks_path = f"{__UPLOADS__}/{notebooks_name}"
235
- log.write_logs(results_path, "Step 2a: Uploaded File Names Determined",
236
- f"notebooks path: {notebooks_path}",
237
- "debug",
238
- 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)
239
186
  fh = open(auto_p, 'wb')
240
187
  fh.write(autograder['body'])
241
188
 
242
189
  fh = open(notebooks_path, 'wb')
243
190
  fh.write(notebooks['body'])
244
- log.write_logs(results_path, "Step 3: Uploaded Files Written to Disk",
245
- f"Results Code: {results_path}",
246
- "debug",
247
- 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)
248
193
  m = "Please save this code. You can retrieve your files by submitting this code "
249
194
  m += f"in the \"Results\" section to the right: {results_path}"
250
195
  self.render("index.html", message=m)
251
196
  try:
252
197
  await g.grade(auto_p, notebooks_path, results_path)
253
198
  except Exception as e:
254
- log.write_logs(results_path, "Grading Problem",
255
- str(e),
256
- "error",
257
- f'{os.environ.get("ENVIRONMENT")}-logs')
199
+ log.write_logs(results_path, "Grading Problem", str(e), "error", log_error)
258
200
  else:
259
- log.write_logs(results_path, "Step 2b: Uploaded Files not given",
260
- "",
261
- "debug",
262
- f'{os.environ.get("ENVIRONMENT")}-debug')
201
+ m = "Step 2b: Uploaded Files not given"
202
+ log.write_logs(results_path, m, "", "debug", log_debug)
263
203
  m = "It looks like you did not set the notebooks or autograder.zip or both!"
264
204
  self.render("index.html", message=m)
265
205
 
266
206
 
267
207
  settings = {
268
208
  "cookie_secret": str(uuid.uuid4()),
269
- "xsrf_cookies": True
209
+ "xsrf_cookies": True,
210
+ "login_url": "/login"
270
211
  }
271
212
 
272
213
  application = tornado.web.Application([
273
- (r"/", Userform),
214
+ (r"/", MainHandler),
215
+ (r"/login", LoginHandler),
274
216
  (r"/upload", Upload),
275
217
  (r"/download", Download),
276
- (r"/auth/github", Userform),
277
- ], **settings, debug=True)
218
+ (r"/oauth_callback", GitHubOAuthHandler),
219
+ ], **settings, debug=False)
278
220
 
279
221
 
280
222
  def main():
@@ -282,14 +224,11 @@ def main():
282
224
  """
283
225
  try:
284
226
  application.listen(80)
285
- msg = f'{os.environ.get("ENVIRONMENT")}-debug'
286
- log.write_logs("Server Start", "Starting Server", "", "info", msg)
227
+ log.write_logs("Server Start", "Starting Server", "", "info", log_debug)
287
228
  tornado.ioloop.IOLoop.instance().start()
288
229
  except Exception as e:
289
- log.write_logs("Server Start Error", "Server Starting error",
290
- str(e),
291
- "error",
292
- f'{os.environ.get("ENVIRONMENT")}-debug')
230
+ m = "Server Starting error"
231
+ log.write_logs("Server Start Error", m, str(e), "error", log_debug)
293
232
 
294
233
 
295
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,16 @@
1
+ github_access_id: ENC[AES256_GCM,data:6othw4MyjLk/gvYAbmGn7cS0xis=,iv:CJcaUgdPanPrNHOsR9pdEl5RippYUqREuI489FLc6l4=,tag:dxCzrJ1L0KXS9B+Gc6yCAg==,type:str]
2
+ github_access_secret: ENC[AES256_GCM,data:QCxHQpy+0hptRZIp6OXxdPGfwb3FaY0be1/TjKJG5BTK06NzWlma2A==,iv:kP6JPkvz1FKoiYSgO0GEw6zTgmRhxIkK2IIaDiUt0rM=,tag:5GOZVUXQMNblwD33A344Ng==,type:str]
3
+ sops:
4
+ kms: []
5
+ gcp_kms:
6
+ - resource_id: projects/data8x-scratch/locations/global/keyRings/data8x-sops/cryptoKeys/otter-service
7
+ created_at: "2024-04-03T20:55:56Z"
8
+ enc: CiQA+mtdjxpP5gJHTfXLOO3LGHaXfPniExMUlgbOPe3j0kTWNZ4SSQB/DdDBy/MSy9vA/EBaN0NtT2d3qz815c1SWt0VNetH6OgfHR4cHAOJURgBOBXiPPg/jCSJNNPAn1vOoa1axWpET9taglLA1g8=
9
+ azure_kv: []
10
+ hc_vault: []
11
+ age: []
12
+ lastmodified: "2024-04-03T20:55:57Z"
13
+ mac: ENC[AES256_GCM,data:dRddLm5uPG7mpTfQuIa5tN5DE9C+Bmxjt5K02/fI/9tor/Ee41Gvmgxda3tjdD4bLUaPsInXAcdBjVXQ0FxvsHp/VIh41WSPAJfb811E0U1yI6OX+EpwJBnnwFVlHkcy+1HrcJZ8bVtXyinm4g+5kjiqRaN4j3dlhpWNzG6/CTw=,iv:R/cR3B/erkBmQTwsmO8zY4WztHUY5VxjsT5AHkwuYPQ=,tag:OhRdDOiEGvyqLSVnY9IcoA==,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:vGx6zpYlPo38qR4QNuWwqfa6E1g=,iv:S7B8SUmSaHHjWwHtzK+4CIUxmmMHWCIU1eSubh3BG1w=,tag:JdxhwhkEb6Y+NUA23X14zg==,type:str]
2
+ github_access_secret: ENC[AES256_GCM,data:XDOjdPt5SkXNQRLALAeNS20CeFWwTCVHlOqDoDiDz6DYHKBq8VJMHQ==,iv:SAm7tj0YwNXnxCQx8b6DVXbevxumdqd6piiDxMTLOLM=,tag:60wtxm7n1iJ8SCU17F2FDQ==,type:str]
3
+ sops:
4
+ kms: []
5
+ gcp_kms:
6
+ - resource_id: projects/data8x-scratch/locations/global/keyRings/data8x-sops/cryptoKeys/otter-service
7
+ created_at: "2024-04-03T20:55:51Z"
8
+ enc: CiQA+mtdj7+426Te3zwawNSQpo18+vkZXnEq23JYHBFSWqzHSkkSSQB/DdDBlBfvc3Ek/8T3or9nzq//ADi3xMBdQ4U9VKnqaQOgmX2aPbwd+or29xIIr4Q8k8SyhdDrcSmF/Yat+hVbUVvtzI4P71Y=
9
+ azure_kv: []
10
+ hc_vault: []
11
+ age: []
12
+ lastmodified: "2024-04-03T20:55:52Z"
13
+ mac: ENC[AES256_GCM,data:LVLldWnSDQ6I09N6+rCIaD+nno5xGs4/eorTY7La+z5dGgpzsW+N8k7xUV+XvL9CB6+1n5YIC/rzpUKD/DtfiijXNbA/kUuSF3OyZ3nrQEzvX0vOBT3qik8vFbhp91OmJqK5r5PpxjET5Okp6enjihdkzpAgSoQjyqjXWsv5npA=,iv:8A062Olw5SYp/odZ8dZcL0tPOWhbJxTj0vbJgbQX+vM=,tag:jLS7Fu/J+i/+mFRifIzsFQ==,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:2IqN4IC3eyM1qxJ7uy6xAEGce5w=,iv:btm2Q/5TOoOnwih2l0Q3QkjGUfx4Biz5XTM1SoGUbOw=,tag:sSpLAnYv/TIXMK4yB/pMPg==,type:str]
2
+ github_access_secret: ENC[AES256_GCM,data:OhQUwHi4tBcMf/GPFohYbdSSD3hWfTODIYh883mSaxDifP72uchHYA==,iv:o7GpOxEJGnD8/ClPEx9XZh07mg3ty91u5wzkiEC0xKk=,tag:oaEFun8sbl2g77oKaTl+kA==,type:str]
3
+ sops:
4
+ kms: []
5
+ gcp_kms:
6
+ - resource_id: projects/data8x-scratch/locations/global/keyRings/data8x-sops/cryptoKeys/otter-service
7
+ created_at: "2024-04-03T20:55:39Z"
8
+ enc: CiQA+mtdj0RKYxCmqihxBuK/BLHCESM8jh8P/7cEn+z2skGDeL0SSQB/DdDBvPjBiL88kvhLS08s4DV0yWvFtpoKLCO3+v2C/dtDO8ojGVUeOtZh0W4L+x9PM+uffXA6ezOsdzO8+k2nmtTYTIacDMU=
9
+ azure_kv: []
10
+ hc_vault: []
11
+ age: []
12
+ lastmodified: "2024-04-03T20:55:40Z"
13
+ mac: ENC[AES256_GCM,data:cszrryu3Ya5TrYJtZZzj8bDW9exvT7zOc3cUmzSSDQAlocz4ayOvJzU7tb7SODrWSA07usLqdzWpAzJ0/DVqw7Psp+Q+e7AzegbWOuoSPTYrFx0iy2p68cJcw3yujSaMyHv4E37wWiPUqvl8XUN+EGbYpnWBRSTaM1F/zXXG3Ko=,iv:LCMDdd007JghnzSHeQgXpsCfqdriZn4BcI3THqTWOIg=,tag:kSu9wzHliVG5QXIA1z0aoQ==,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:kSFOqwFCOEBzp3aA+xztYZna+DQ=,iv:/ZMoKSWI/KeMQZe6PDQe4MF6XIo1cpRc2spt3HAelpY=,tag:a28+3LzpVadaXC4P+S8EVQ==,type:str]
2
+ github_access_secret: ENC[AES256_GCM,data:RzYBaHSYuqDvEJ4CMMdtIpOb90ObcrnRSAOhin/fLVM+Xf2DDvxxkA==,iv:thm1AlQjjUtm5gmeliAeIlbY3kWhdHnx8d5cWLMhSSs=,tag:Dl8ZjC8j4w8HkJ5rOP5phg==,type:str]
3
+ sops:
4
+ kms: []
5
+ gcp_kms:
6
+ - resource_id: projects/data8x-scratch/locations/global/keyRings/data8x-sops/cryptoKeys/otter-service
7
+ created_at: "2024-04-03T20:56:00Z"
8
+ enc: CiQA+mtdj6rYoGKDso+uJhKHBwVaeG452KHDn7K0Wz2QgFBF/G8SSQB/DdDB7uXjR3oYouuiC1pVprd7vqGbWrA4Nmg6kdrjUYCZ8cHHrbTSlTvzJ9C+2dTRwAWmSvSsFn2L7Yj31Vnujxf8Ong9LOM=
9
+ azure_kv: []
10
+ hc_vault: []
11
+ age: []
12
+ lastmodified: "2024-04-03T20:56:01Z"
13
+ mac: ENC[AES256_GCM,data:r66eFRpcST3IYgQ86Qy4YYXA/BjWCQIDz+iK1L9BSMtHDW6t8GphIj2kRBjpFK7bQWt7CKExXwTgv/2JjedcYiulGUEANqeZuRZjpfUuas9yA9mibklzg2+b5O++s/URl7wcAceFbg7PD6ddn2cb8t8nzqDw7cQPjblmS50H5aA=,iv:gdqHI0kGj5AAd47VTZrE6OabMjeFMxfX7KiJV1gjbGo=,tag:G+79+A6d4SL9iA/LxjAhrA==,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
2
  Name: otter_service_stdalone
3
- Version: 0.1.16
3
+ Version: 0.1.19
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=cAJAbAh288a9AL-3yxwFzEM1L26izSJ6wma5aiml_9Y,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=ORUVDu8SDcv0OE2ThwROppeg7y8oLkJJbPTCMn0s5l0,1138
10
+ otter_service_stdalone/secrets/gh_key.local.yaml,sha256=NtPXXyGf1iSgJ9Oa2ahvIEf_fcmflB3dwd3LWyCgxis,1138
11
+ otter_service_stdalone/secrets/gh_key.prod.yaml,sha256=6vgLqHzDp8cVAOJlpSXGDTUjSI6EyCb6f1-SSVG2rqw,1138
12
+ otter_service_stdalone/secrets/gh_key.staging.yaml,sha256=cKVqj4dcwuz2LhXwMNQy_1skF8XCVQOX2diXNjAFJXg,1138
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.19.dist-info/METADATA,sha256=DmAEzyj4My9WP-U4Zs1bM-14CMpuzXt1sslkfblLBlc,1345
16
+ otter_service_stdalone-0.1.19.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
17
+ otter_service_stdalone-0.1.19.dist-info/entry_points.txt,sha256=cx447chuIEl8ly9jEoF5-2xNhaKsWcIMDdhUMH_00qQ,75
18
+ otter_service_stdalone-0.1.19.dist-info/top_level.txt,sha256=6UP22fD4OhbLt23E01v8Kvjn44vPRbyTIg_GqMYL-Ng,23
19
+ otter_service_stdalone-0.1.19.dist-info/RECORD,,
@@ -1,11 +0,0 @@
1
- otter_service_stdalone/__init__.py,sha256=yF88-8vL8keLe6gCTumymw0UoMkWkSrJnzLru4zBCLQ,23
2
- otter_service_stdalone/access_sops_keys.py,sha256=2f3vszsF6LojCmt3AmInyZvIoGrWnjNlu9mVOXmWvcY,2149
3
- otter_service_stdalone/app.py,sha256=do03zXIU_1xeBpiMuC0JhbToNA7zuwSgWc0z4i7UtKc,13011
4
- otter_service_stdalone/fs_logging.py,sha256=LTrgadyjQyZ204sS5B1HtchnBV49SKCpfrJdcfuvApo,2417
5
- otter_service_stdalone/index.html,sha256=QbSQs31OZhWlCQFE5vvJOlNh-JHEJ3PZPgR4GukzrCA,6032
6
- otter_service_stdalone/upload_handle.py,sha256=NB6isuLrLkUCPetUA3ugUlKKpAYw4nvDBVmxpzvgcE8,5157
7
- otter_service_stdalone-0.1.16.dist-info/METADATA,sha256=MTcibeq-t56l0XGNF4d_QhptZ_fO2Ys6n8ki_SDSDgM,1345
8
- otter_service_stdalone-0.1.16.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
9
- otter_service_stdalone-0.1.16.dist-info/entry_points.txt,sha256=cx447chuIEl8ly9jEoF5-2xNhaKsWcIMDdhUMH_00qQ,75
10
- otter_service_stdalone-0.1.16.dist-info/top_level.txt,sha256=6UP22fD4OhbLt23E01v8Kvjn44vPRbyTIg_GqMYL-Ng,23
11
- otter_service_stdalone-0.1.16.dist-info/RECORD,,