3
3
use crate :: error:: PythonError ;
4
4
use crate :: policy:: Python3StudentFilePolicy ;
5
5
use crate :: python_test_result:: PythonTestResult ;
6
+ use hmac:: { Hmac , Mac , NewMac } ;
6
7
use lazy_static:: lazy_static;
8
+ use rand:: Rng ;
9
+ use sha2:: Sha256 ;
7
10
use std:: collections:: { HashMap , HashSet } ;
8
11
use std:: env;
9
12
use std:: io:: BufReader ;
@@ -102,6 +105,7 @@ impl Python3Plugin {
102
105
path : & Path ,
103
106
extra_args : & [ & str ] ,
104
107
timeout : Option < Duration > ,
108
+ stdin : Option < String > ,
105
109
) -> Result < Output , PythonError > {
106
110
let minimum_python_version = TmcProjectYml :: from ( path) ?
107
111
. minimum_python_version
@@ -139,6 +143,12 @@ impl Python3Plugin {
139
143
140
144
let command = Self :: get_local_python_command ( ) ;
141
145
let command = command. with ( |e| e. args ( & common_args) . args ( extra_args) . cwd ( path) ) ;
146
+ let command = if let Some ( stdin) = stdin {
147
+ command. set_stdin_data ( stdin)
148
+ } else {
149
+ command
150
+ } ;
151
+
142
152
let output = if let Some ( timeout) = timeout {
143
153
command. output_with_timeout ( timeout) ?
144
154
} else {
@@ -166,14 +176,25 @@ impl Python3Plugin {
166
176
}
167
177
168
178
/// Parse test result file
169
- fn parse_test_result (
179
+ fn parse_and_verify_test_result (
170
180
test_results_json : & Path ,
171
181
logs : HashMap < String , String > ,
182
+ hmac_data : Option < ( String , String ) > ,
172
183
) -> Result < RunResult , PythonError > {
173
- let results_file = file_util:: open_file ( & test_results_json) ?;
174
- let test_results: Vec < PythonTestResult > =
175
- serde_json:: from_reader ( BufReader :: new ( results_file) )
176
- . map_err ( |e| PythonError :: Deserialize ( test_results_json. to_path_buf ( ) , e) ) ?;
184
+ let results = file_util:: read_file_to_string ( & test_results_json) ?;
185
+
186
+ // verify test results
187
+ if let Some ( ( hmac_secret, test_runner_hmac_hex) ) = hmac_data {
188
+ let mut mac = Hmac :: < Sha256 > :: new_varkey ( hmac_secret. as_bytes ( ) )
189
+ . expect ( "HMAC can take key of any size" ) ;
190
+ mac. update ( results. as_bytes ( ) ) ;
191
+ let bytes =
192
+ hex:: decode ( & test_runner_hmac_hex) . map_err ( |_| PythonError :: UnexpectedHmac ) ?;
193
+ mac. verify ( & bytes) . map_err ( |_| PythonError :: InvalidHmac ) ?;
194
+ }
195
+
196
+ let test_results: Vec < PythonTestResult > = serde_json:: from_str ( & results)
197
+ . map_err ( |e| PythonError :: Deserialize ( test_results_json. to_path_buf ( ) , e) ) ?;
177
198
178
199
let mut status = RunStatus :: Passed ;
179
200
let mut failed_points = HashSet :: new ( ) ;
@@ -209,7 +230,8 @@ impl LanguagePlugin for Python3Plugin {
209
230
file_util:: remove_file ( & available_points_json) ?;
210
231
}
211
232
212
- let run_result = Self :: run_tmc_command ( exercise_directory, & [ "available_points" ] , None ) ;
233
+ let run_result =
234
+ Self :: run_tmc_command ( exercise_directory, & [ "available_points" ] , None , None ) ;
213
235
if let Err ( error) = run_result {
214
236
log:: error!( "Failed to scan exercise. {}" , error) ;
215
237
}
@@ -233,7 +255,24 @@ impl LanguagePlugin for Python3Plugin {
233
255
file_util:: remove_file ( & test_results_json) ?;
234
256
}
235
257
236
- let output = Self :: run_tmc_command ( exercise_directory, & [ ] , timeout) ;
258
+ let ( output, random_string) = if exercise_directory. join ( "tmc/hmac_writer.py" ) . exists ( ) {
259
+ // has hmac writer
260
+ let random_string: String = rand:: thread_rng ( )
261
+ . sample_iter ( rand:: distributions:: Alphanumeric )
262
+ . take ( 32 )
263
+ . map ( char:: from)
264
+ . collect ( ) ;
265
+ let output = Self :: run_tmc_command (
266
+ exercise_directory,
267
+ & [ "--wait-for-secret" ] ,
268
+ timeout,
269
+ Some ( random_string. clone ( ) ) ,
270
+ ) ;
271
+ ( output, Some ( random_string) )
272
+ } else {
273
+ let output = Self :: run_tmc_command ( exercise_directory, & [ ] , timeout, None ) ;
274
+ ( output, None )
275
+ } ;
237
276
238
277
match output {
239
278
Ok ( output) => {
@@ -246,7 +285,17 @@ impl LanguagePlugin for Python3Plugin {
246
285
"stderr" . to_string ( ) ,
247
286
String :: from_utf8_lossy ( & output. stderr ) . into_owned ( ) ,
248
287
) ;
249
- let parse_res = Self :: parse_test_result ( & test_results_json, logs) ;
288
+
289
+ let hmac_data = if let Some ( random_string) = random_string {
290
+ let hmac_result_path = exercise_directory. join ( ".tmc_test_results.hmac.sha256" ) ;
291
+ let test_runner_hmac = file_util:: read_file_to_string ( hmac_result_path) ?;
292
+ Some ( ( random_string, test_runner_hmac) )
293
+ } else {
294
+ None
295
+ } ;
296
+
297
+ let parse_res =
298
+ Self :: parse_and_verify_test_result ( & test_results_json, logs, hmac_data) ;
250
299
// remove file regardless of parse success
251
300
if test_results_json. exists ( ) {
252
301
file_util:: remove_file ( & test_results_json) ?;
@@ -366,7 +415,10 @@ impl LanguagePlugin for Python3Plugin {
366
415
#[ cfg( test) ]
367
416
mod test {
368
417
use super :: * ;
369
- use std:: path:: { Path , PathBuf } ;
418
+ use std:: {
419
+ io:: Write ,
420
+ path:: { Path , PathBuf } ,
421
+ } ;
370
422
use tmc_langs_framework:: zip:: ZipArchive ;
371
423
use tmc_langs_framework:: { domain:: RunStatus , plugin:: LanguagePlugin } ;
372
424
@@ -752,4 +804,40 @@ class TestClass(unittest.TestCase):
752
804
}
753
805
assert ! ( got_point) ;
754
806
}
807
+
808
+ #[ test]
809
+ fn verifies_test_results_success ( ) {
810
+ init ( ) ;
811
+
812
+ let mut temp = tempfile:: NamedTempFile :: new ( ) . unwrap ( ) ;
813
+ temp. write_all ( br#"[{"name": "test.test_hello_world.HelloWorld.test_first", "status": "passed", "message": "", "passed": true, "points": ["p01-01.1"], "backtrace": []}]"# ) . unwrap ( ) ;
814
+
815
+ let hmac_secret = "047QzQx8RAYLR3lf0UfB75WX5EFnx7AV" . to_string ( ) ;
816
+ let test_runner_hmac =
817
+ "b379817c66cc7b1610d03ac263f02fa11f7b0153e6aeff3262ecc0598bf0be21" . to_string ( ) ;
818
+ Python3Plugin :: parse_and_verify_test_result (
819
+ temp. path ( ) ,
820
+ HashMap :: new ( ) ,
821
+ Some ( ( hmac_secret, test_runner_hmac) ) ,
822
+ )
823
+ . unwrap ( ) ;
824
+ }
825
+
826
+ #[ test]
827
+ fn verifies_test_results_failure ( ) {
828
+ init ( ) ;
829
+
830
+ let mut temp = tempfile:: NamedTempFile :: new ( ) . unwrap ( ) ;
831
+ temp. write_all ( br#"[{"name": "test.test_hello_world.HelloWorld.test_first", "status": "passed", "message": "", "passed": true, "points": ["p01-01.1"], "backtrace": []}]"# ) . unwrap ( ) ;
832
+
833
+ let hmac_secret = "047QzQx8RAYLR3lf0UfB75WX5EFnx7AV" . to_string ( ) ;
834
+ let test_runner_hmac =
835
+ "b379817c66cc7b1610d03ac263f02fa11f7b0153e6aeff3262ecc0598bf0be22" . to_string ( ) ;
836
+ let res = Python3Plugin :: parse_and_verify_test_result (
837
+ temp. path ( ) ,
838
+ HashMap :: new ( ) ,
839
+ Some ( ( hmac_secret, test_runner_hmac) ) ,
840
+ ) ;
841
+ assert ! ( res. is_err( ) ) ;
842
+ }
755
843
}
0 commit comments