Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion tmc-langs-cli/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,10 @@ pub fn create_app() -> App<'static, 'static> {
.arg(Arg::with_name("output-path")
.help("If defined, the test results will be written to this path. Overwritten if it already exists.")
.long("output-path")
.takes_value(true)))
.takes_value(true))
.arg(Arg::with_name("wait-for-secret")
.help("If defined, the command will wait for a string to be written to stdin, used for signing the output file with jwt.")
.long("wait-for-secret")))

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

Expand Down
27 changes: 21 additions & 6 deletions tmc-langs-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ use self::output::{
use anyhow::{Context, Result};
use clap::{ArgMatches, Error, ErrorKind};
use serde::Serialize;
use std::collections::HashMap;
use std::fs::File;
use std::io::{Read, Write};
use std::ops::Deref;
use std::path::{Path, PathBuf};
use std::{collections::HashMap, io::stdin};
use std::{env, io::Cursor};
use tmc_langs::{file_util, notification_reporter, CommandError, StyleValidationResult};
use tmc_langs::{
Expand Down Expand Up @@ -356,7 +356,7 @@ fn run_app(matches: ArgMatches, pretty: bool) -> Result<()> {
})?;

if let Some(output_path) = output_path {
write_result_to_file_as_json(&exercises, output_path, pretty)?;
write_result_to_file_as_json(&exercises, output_path, pretty, None)?;
}

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

if let Some(output_path) = output_path {
write_result_to_file_as_json(&config, output_path, pretty)?;
write_result_to_file_as_json(&config, output_path, pretty, None)?;
}

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

let wait_for_secret = matches.is_present("wait-for-secret");

let secret = if wait_for_secret {
let mut s = String::new();
stdin().read_line(&mut s)?;
Some(s.trim().to_string())
} else {
None
};

file_util::lock!(exercise_path);

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

if let Some(output_path) = output_path {
write_result_to_file_as_json(&test_result, output_path, pretty)?;
write_result_to_file_as_json(&test_result, output_path, pretty, secret)?;
}

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

if let Some(output_path) = output_path {
write_result_to_file_as_json(&scan_result, output_path, pretty)?;
write_result_to_file_as_json(&scan_result, output_path, pretty, None)?;
}

let output = Output::finished_with_data(
Expand Down Expand Up @@ -1266,6 +1276,7 @@ fn write_result_to_file_as_json<T: Serialize>(
result: &T,
output_path: &Path,
pretty: bool,
secret: Option<String>,
) -> Result<()> {
let mut output_file = file_util::create_file_lock(output_path).with_context(|| {
format!(
Expand All @@ -1275,7 +1286,11 @@ fn write_result_to_file_as_json<T: Serialize>(
})?;
let guard = output_file.lock()?;

if pretty {
if let Some(secret) = secret {
let token = tmc_langs::sign_with_jwt(result, secret.as_bytes())?;
file_util::write_to_writer(token, guard.deref())
.with_context(|| format!("Failed to write result to {}", output_path.display()))?;
} else if pretty {
serde_json::to_writer_pretty(guard.deref(), result).with_context(|| {
format!(
"Failed to write result as JSON to {}",
Expand Down
2 changes: 2 additions & 0 deletions tmc-langs-util/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ pub enum FileError {
NoFileName(PathBuf),
#[error("Expected {0} to be a directory, but it was a file")]
UnexpectedFile(PathBuf),
#[error("Failed to write data")]
WriteError(#[source] std::io::Error),

// lock errors
#[error("Failed to lock file at path {0}")]
Expand Down
10 changes: 10 additions & 0 deletions tmc-langs-util/src/file_util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,16 @@ pub fn write_to_file<S: AsRef<[u8]>, P: AsRef<Path>>(
Ok(target_file)
}

pub fn write_to_writer<S: AsRef<[u8]>, W: Write>(
source: S,
mut target: W,
) -> Result<(), FileError> {
target
.write_all(source.as_ref())
.map_err(|e| FileError::WriteError(e))?;
Ok(())
}

/// Reads all of the data from source and writes it into a new file at target.
pub fn read_to_file<R: Read, P: AsRef<Path>>(source: &mut R, target: P) -> Result<File, FileError> {
let target = target.as_ref();
Expand Down
5 changes: 4 additions & 1 deletion tmc-langs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,20 @@ tmc-langs-util = { path = "../tmc-langs-util" }
base64 = "0.13"
dirs = "3"
# heim = { version = "0.1.0-beta.3", features = ["disk"] }
hmac = { version = "0.10", features = ["std"] }
impl-enum = "0.2"
jwt = "0.13"
log = "0.4"
lazy_static = "1"
md5 = "0.7"
oauth2 = { version = "4.0.0-alpha.3", features = ["reqwest"] }
regex = "1"
rpassword = "5"
schemars = "0.8"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
serde_yaml = "0.8"
schemars = "0.8"
sha2 = "0.9"
shellwords = "1"
smol = "1"
tar = "0.4"
Expand Down
4 changes: 4 additions & 0 deletions tmc-langs/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,10 @@ pub enum LangsError {
TomlDeserialize(#[from] toml::de::Error),
#[error(transparent)]
Json(#[from] serde_json::Error),
#[error(transparent)]
Jwt(#[from] jwt::Error),
#[error(transparent)]
Hmac(#[from] hmac::crypto_mac::InvalidKeyLength),
}

/// Error validating TMC params values.
Expand Down
34 changes: 34 additions & 0 deletions tmc-langs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ pub use crate::data::{
pub use crate::error::{LangsError, ParamError};
pub use crate::submission_packaging::prepare_submission;
pub use crate::submission_processing::prepare_solution;
use hmac::{Hmac, NewMac};
use serde::Serialize;
use sha2::Sha256;
pub use tmc_client::{
ClientError, ClientUpdateData, Course, CourseData, CourseDetails, CourseExercise,
ExerciseDetails, FeedbackAnswer, NewSubmission, Organization, Review, RunResult,
Expand All @@ -38,6 +41,7 @@ pub use tmc_langs_util::{
use crate::config::ProjectsConfig;
use crate::data::DownloadTarget;
// use heim::disk;
use jwt::SignWithKey;
use oauth2::{
basic::BasicTokenType, AccessToken, EmptyExtraTokenFields, Scope, StandardTokenResponse,
};
Expand All @@ -55,6 +59,12 @@ use toml::{map::Map as TomlMap, Value as TomlValue};
use url::Url;
use walkdir::WalkDir;

pub fn sign_with_jwt<T: Serialize>(value: T, secret: &[u8]) -> Result<String, LangsError> {
let key: Hmac<Sha256> = Hmac::new_varkey(secret)?;
let token = value.sign_with_key(&key)?;
Ok(token)
}

/// Returns the projects directory for the given client name.
pub fn get_projects_dir(client_name: &str) -> Result<PathBuf, LangsError> {
let config_path = TmcConfig::get_location(client_name)?;
Expand Down Expand Up @@ -883,3 +893,27 @@ fn extract_project_overwrite(
)?;
Ok(())
}

#[cfg(test)]
mod test {
use super::*;

fn init() {
use log::*;
use simple_logger::*;
let _ = SimpleLogger::new().with_level(LevelFilter::Debug).init();
}

#[test]
fn signs_with_jwt() {
init();

let value = "some string";
let secret = "some secret".as_bytes();
let signed = sign_with_jwt(value, secret).unwrap();
assert_eq!(
signed,
"eyJhbGciOiJIUzI1NiJ9.InNvbWUgc3RyaW5nIg.FfWkq8BeQRe2vlrfLbJHObFAslXqK5_V_hH2TbBqggc"
);
}
}