Skip to content

Commit fb7e2e2

Browse files
committed
added option to sign test results with jwt
1 parent 50f104d commit fb7e2e2

File tree

7 files changed

+79
-8
lines changed

7 files changed

+79
-8
lines changed

tmc-langs-cli/src/app.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,10 @@ pub fn create_app() -> App<'static, 'static> {
257257
.arg(Arg::with_name("output-path")
258258
.help("If defined, the test results will be written to this path. Overwritten if it already exists.")
259259
.long("output-path")
260-
.takes_value(true)))
260+
.takes_value(true))
261+
.arg(Arg::with_name("wait-for-secret")
262+
.help("If defined, the command will wait for a string to be written to stdin, used for signing the output file with jwt.")
263+
.long("wait-for-secret")))
261264

262265
.subcommand(create_settings_app()) // "settings"
263266

tmc-langs-cli/src/lib.rs

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@ use self::output::{
1313
use anyhow::{Context, Result};
1414
use clap::{ArgMatches, Error, ErrorKind};
1515
use serde::Serialize;
16-
use std::collections::HashMap;
1716
use std::fs::File;
1817
use std::io::{Read, Write};
1918
use std::ops::Deref;
2019
use std::path::{Path, PathBuf};
20+
use std::{collections::HashMap, io::stdin};
2121
use std::{env, io::Cursor};
2222
use tmc_langs::{file_util, notification_reporter, CommandError, StyleValidationResult};
2323
use tmc_langs::{
@@ -356,7 +356,7 @@ fn run_app(matches: ArgMatches, pretty: bool) -> Result<()> {
356356
})?;
357357

358358
if let Some(output_path) = output_path {
359-
write_result_to_file_as_json(&exercises, output_path, pretty)?;
359+
write_result_to_file_as_json(&exercises, output_path, pretty, None)?;
360360
}
361361

362362
let output = Output::finished_with_data(
@@ -383,7 +383,7 @@ fn run_app(matches: ArgMatches, pretty: bool) -> Result<()> {
383383
})?;
384384

385385
if let Some(output_path) = output_path {
386-
write_result_to_file_as_json(&config, output_path, pretty)?;
386+
write_result_to_file_as_json(&config, output_path, pretty, None)?;
387387
}
388388

389389
let output = Output::finished_with_data(
@@ -568,6 +568,16 @@ fn run_app(matches: ArgMatches, pretty: bool) -> Result<()> {
568568
let output_path = matches.value_of("output-path");
569569
let output_path = output_path.map(Path::new);
570570

571+
let wait_for_secret = matches.is_present("wait-for-secret");
572+
573+
let secret = if wait_for_secret {
574+
let mut s = String::new();
575+
stdin().read_line(&mut s)?;
576+
Some(s.trim().to_string())
577+
} else {
578+
None
579+
};
580+
571581
file_util::lock!(exercise_path);
572582

573583
let test_result = tmc_langs::run_tests(exercise_path).with_context(|| {
@@ -589,7 +599,7 @@ fn run_app(matches: ArgMatches, pretty: bool) -> Result<()> {
589599
};
590600

591601
if let Some(output_path) = output_path {
592-
write_result_to_file_as_json(&test_result, output_path, pretty)?;
602+
write_result_to_file_as_json(&test_result, output_path, pretty, secret)?;
593603
}
594604

595605
// todo: checkstyle results in stdout?
@@ -635,7 +645,7 @@ fn run_app(matches: ArgMatches, pretty: bool) -> Result<()> {
635645
})?;
636646

637647
if let Some(output_path) = output_path {
638-
write_result_to_file_as_json(&scan_result, output_path, pretty)?;
648+
write_result_to_file_as_json(&scan_result, output_path, pretty, None)?;
639649
}
640650

641651
let output = Output::finished_with_data(
@@ -1266,6 +1276,7 @@ fn write_result_to_file_as_json<T: Serialize>(
12661276
result: &T,
12671277
output_path: &Path,
12681278
pretty: bool,
1279+
secret: Option<String>,
12691280
) -> Result<()> {
12701281
let mut output_file = file_util::create_file_lock(output_path).with_context(|| {
12711282
format!(
@@ -1275,7 +1286,11 @@ fn write_result_to_file_as_json<T: Serialize>(
12751286
})?;
12761287
let guard = output_file.lock()?;
12771288

1278-
if pretty {
1289+
if let Some(secret) = secret {
1290+
let token = tmc_langs::sign_with_jwt(result, secret.as_bytes())?;
1291+
file_util::write_to_writer(token, guard.deref())
1292+
.with_context(|| format!("Failed to write result to {}", output_path.display()))?;
1293+
} else if pretty {
12791294
serde_json::to_writer_pretty(guard.deref(), result).with_context(|| {
12801295
format!(
12811296
"Failed to write result as JSON to {}",

tmc-langs-util/src/error.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ pub enum FileError {
4141
NoFileName(PathBuf),
4242
#[error("Expected {0} to be a directory, but it was a file")]
4343
UnexpectedFile(PathBuf),
44+
#[error("Failed to write data")]
45+
WriteError(#[source] std::io::Error),
4446

4547
// lock errors
4648
#[error("Failed to lock file at path {0}")]

tmc-langs-util/src/file_util.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,16 @@ pub fn write_to_file<S: AsRef<[u8]>, P: AsRef<Path>>(
144144
Ok(target_file)
145145
}
146146

147+
pub fn write_to_writer<S: AsRef<[u8]>, W: Write>(
148+
source: S,
149+
mut target: W,
150+
) -> Result<(), FileError> {
151+
target
152+
.write_all(source.as_ref())
153+
.map_err(|e| FileError::WriteError(e))?;
154+
Ok(())
155+
}
156+
147157
/// Reads all of the data from source and writes it into a new file at target.
148158
pub fn read_to_file<R: Read, P: AsRef<Path>>(source: &mut R, target: P) -> Result<File, FileError> {
149159
let target = target.as_ref();

tmc-langs/Cargo.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,20 @@ tmc-langs-util = { path = "../tmc-langs-util" }
1414
base64 = "0.13"
1515
dirs = "3"
1616
# heim = { version = "0.1.0-beta.3", features = ["disk"] }
17+
hmac = { version = "0.10", features = ["std"] }
1718
impl-enum = "0.2"
19+
jwt = "0.13"
1820
log = "0.4"
1921
lazy_static = "1"
2022
md5 = "0.7"
2123
oauth2 = { version = "4.0.0-alpha.3", features = ["reqwest"] }
2224
regex = "1"
2325
rpassword = "5"
26+
schemars = "0.8"
2427
serde = { version = "1", features = ["derive"] }
2528
serde_json = "1"
2629
serde_yaml = "0.8"
27-
schemars = "0.8"
30+
sha2 = "0.9"
2831
shellwords = "1"
2932
smol = "1"
3033
tar = "0.4"

tmc-langs/src/error.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,10 @@ pub enum LangsError {
9797
TomlDeserialize(#[from] toml::de::Error),
9898
#[error(transparent)]
9999
Json(#[from] serde_json::Error),
100+
#[error(transparent)]
101+
Jwt(#[from] jwt::Error),
102+
#[error(transparent)]
103+
Hmac(#[from] hmac::crypto_mac::InvalidKeyLength),
100104
}
101105

102106
/// Error validating TMC params values.

tmc-langs/src/lib.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ pub use crate::data::{
2121
pub use crate::error::{LangsError, ParamError};
2222
pub use crate::submission_packaging::prepare_submission;
2323
pub use crate::submission_processing::prepare_solution;
24+
use hmac::{Hmac, NewMac};
25+
use serde::Serialize;
26+
use sha2::Sha256;
2427
pub use tmc_client::{
2528
ClientError, ClientUpdateData, Course, CourseData, CourseDetails, CourseExercise,
2629
ExerciseDetails, FeedbackAnswer, NewSubmission, Organization, Review, RunResult,
@@ -38,6 +41,7 @@ pub use tmc_langs_util::{
3841
use crate::config::ProjectsConfig;
3942
use crate::data::DownloadTarget;
4043
// use heim::disk;
44+
use jwt::SignWithKey;
4145
use oauth2::{
4246
basic::BasicTokenType, AccessToken, EmptyExtraTokenFields, Scope, StandardTokenResponse,
4347
};
@@ -55,6 +59,12 @@ use toml::{map::Map as TomlMap, Value as TomlValue};
5559
use url::Url;
5660
use walkdir::WalkDir;
5761

62+
pub fn sign_with_jwt<T: Serialize>(value: T, secret: &[u8]) -> Result<String, LangsError> {
63+
let key: Hmac<Sha256> = Hmac::new_varkey(secret)?;
64+
let token = value.sign_with_key(&key)?;
65+
Ok(token)
66+
}
67+
5868
/// Returns the projects directory for the given client name.
5969
pub fn get_projects_dir(client_name: &str) -> Result<PathBuf, LangsError> {
6070
let config_path = TmcConfig::get_location(client_name)?;
@@ -883,3 +893,27 @@ fn extract_project_overwrite(
883893
)?;
884894
Ok(())
885895
}
896+
897+
#[cfg(test)]
898+
mod test {
899+
use super::*;
900+
901+
fn init() {
902+
use log::*;
903+
use simple_logger::*;
904+
let _ = SimpleLogger::new().with_level(LevelFilter::Debug).init();
905+
}
906+
907+
#[test]
908+
fn signs_with_jwt() {
909+
init();
910+
911+
let value = "some string";
912+
let secret = "some secret".as_bytes();
913+
let signed = sign_with_jwt(value, secret).unwrap();
914+
assert_eq!(
915+
signed,
916+
"eyJhbGciOiJIUzI1NiJ9.InNvbWUgc3RyaW5nIg.FfWkq8BeQRe2vlrfLbJHObFAslXqK5_V_hH2TbBqggc"
917+
);
918+
}
919+
}

0 commit comments

Comments
 (0)