Commit 29764829 authored by Robert Czechowski's avatar Robert Czechowski
Browse files

Merge branch 'v1.7' into deploy

parents a9e54f35 7c39b28c
Pipeline #949 failed with stages
in 16 minutes and 47 seconds
...@@ -13,7 +13,6 @@ config.json ...@@ -13,7 +13,6 @@ config.json
*.pdf *.pdf
*.json *.json
*.yaml *.yaml
*.sql
*.sqlite *.sqlite
*.csv *.csv
/bulma /bulma
...@@ -1064,7 +1064,7 @@ checksum = "79c56d6a0b07f9e19282511c83fc5b086364cbae4ba8c7d5f190c3d9b0425a48" ...@@ -1064,7 +1064,7 @@ checksum = "79c56d6a0b07f9e19282511c83fc5b086364cbae4ba8c7d5f190c3d9b0425a48"
[[package]] [[package]]
name = "medal" name = "medal"
version = "1.6.0" version = "1.7.0"
dependencies = [ dependencies = [
"bcrypt", "bcrypt",
"csv", "csv",
......
[package] [package]
version = "1.6.0" version = "1.7.0"
name = "medal" name = "medal"
authors = ["Robert Czechowski <czechowski@bwinf.de>", "Daniel Brüning <bruening@bwinf.de>"] authors = ["Robert Czechowski <czechowski@bwinf.de>", "Daniel Brüning <bruening@bwinf.de>"]
......
...@@ -20,7 +20,7 @@ format: src/db_conn_postgres.rs ...@@ -20,7 +20,7 @@ format: src/db_conn_postgres.rs
cargo +nightly fmt cargo +nightly fmt
clippy: src/db_conn_postgres.rs clippy: src/db_conn_postgres.rs
cargo clippy --all-targets --features 'complete debug' -- -D warnings -A clippy::type-complexity -A clippy::option-map-unit-fn -A clippy::len-zero -A clippy::option-as-ref-deref -A clippy::or-fun-call cargo clippy --all-targets --features 'complete debug' -- -D warnings -A clippy::type-complexity -A clippy::option-map-unit-fn -A clippy::len-zero -A clippy::option-as-ref-deref -A clippy::or-fun-call -A clippy::comparison-to-empty -A clippy::result-unit-err
src/db_conn_postgres.rs: src/db_conn_warning_header.txt src/db_conn_sqlite_new.header.rs src/db_conn_postgres.header.rs src/db_conn.base.rs src/db_conn_postgres.rs: src/db_conn_warning_header.txt src/db_conn_sqlite_new.header.rs src/db_conn_postgres.header.rs src/db_conn.base.rs
cd src; ./generate_connectors.sh cd src; ./generate_connectors.sh
......
ALTER TABLE contest ADD COLUMN requires_contest TEXT;
ALTER TABLE usergroup ADD COLUMN group_created TIMESTAMP;
UPDATE usergroup SET group_created = NOW() WHERE group_created IS NULL;
ALTER TABLE session ADD COLUMN account_created TIMESTAMP;
UPDATE session SET account_created = NOW() WHERE account_created IS NULL;
ALTER TABLE contest ADD COLUMN requires_contest TEXT;
ALTER TABLE usergroup ADD COLUMN group_created TEXT;
UPDATE usergroup SET group_created = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE group_created IS NULL;
ALTER TABLE session ADD COLUMN account_created TEXT;
UPDATE session SET account_created = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE account_created IS NULL;
...@@ -12,18 +12,31 @@ ...@@ -12,18 +12,31 @@
* You should have received a copy of the GNU Affero General Public License along with this program. If not, see * * You should have received a copy of the GNU Affero General Public License along with this program. If not, see *
\* <http://www.gnu.org/licenses/>. */ \* <http://www.gnu.org/licenses/>. */
use oauth_provider;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use structopt::StructOpt; use structopt::StructOpt;
#[derive(Serialize, Deserialize, Clone, Default, Debug)]
pub struct OauthProvider {
pub provider_id: String,
pub medal_oauth_type: String,
pub url: String,
pub client_id: String,
pub client_secret: String,
pub access_token_url: String,
pub user_data_url: String,
pub school_data_url: Option<String>,
pub school_data_secret: Option<String>,
pub allow_teacher_login_without_school: Option<bool>,
pub login_link_text: String,
}
#[derive(Serialize, Deserialize, Clone, Default, Debug)] #[derive(Serialize, Deserialize, Clone, Default, Debug)]
pub struct Config { pub struct Config {
pub host: Option<String>, pub host: Option<String>,
pub port: Option<u16>, pub port: Option<u16>,
pub self_url: Option<String>, pub self_url: Option<String>,
pub oauth_providers: Option<Vec<oauth_provider::OauthProvider>>, pub oauth_providers: Option<Vec<OauthProvider>>,
pub database_file: Option<PathBuf>, pub database_file: Option<PathBuf>,
pub database_url: Option<String>, pub database_url: Option<String>,
pub template: Option<String>, pub template: Option<String>,
......
...@@ -27,6 +27,7 @@ struct ContestYaml { ...@@ -27,6 +27,7 @@ struct ContestYaml {
public_listing: Option<bool>, public_listing: Option<bool>,
requires_login: Option<bool>, requires_login: Option<bool>,
requires_contest: Option<Vec<String>>,
secret: Option<String>, secret: Option<String>,
message: Option<String>, message: Option<String>,
...@@ -62,6 +63,7 @@ pub fn parse_yaml(content: &str, filename: &str, directory: &str) -> Option<Cont ...@@ -62,6 +63,7 @@ pub fn parse_yaml(content: &str, filename: &str, directory: &str) -> Option<Cont
config.max_grade, config.max_grade,
config.position, config.position,
config.requires_login, config.requires_login,
config.requires_contest.map(|list| list.join(",")),
config.secret, config.secret,
config.message); config.message);
// TODO: Timeparsing should fail more pleasantly (-> Panic, thus shows message) // TODO: Timeparsing should fail more pleasantly (-> Panic, thus shows message)
......
...@@ -21,7 +21,7 @@ use db_objects::OptionSession; ...@@ -21,7 +21,7 @@ use db_objects::OptionSession;
use db_objects::SessionUser; use db_objects::SessionUser;
use db_objects::{Contest, Grade, Group, Participation, Submission, Taskgroup}; use db_objects::{Contest, Grade, Group, Participation, Submission, Taskgroup};
use helpers; use helpers;
use oauth_provider::OauthProvider; use config::OauthProvider;
use webfw_iron::{json_val, to_json}; use webfw_iron::{json_val, to_json};
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
...@@ -286,6 +286,18 @@ pub struct ContestStartConstraints { ...@@ -286,6 +286,18 @@ pub struct ContestStartConstraints {
pub grade_matching: bool, pub grade_matching: bool,
} }
fn check_contest_qualification<T: MedalConnection>(conn: &T, session: &SessionUser, contest: &Contest) -> Option<bool> {
let required_contests = contest.requires_contest.as_ref()?.split(',');
for req_contest in required_contests {
if conn.has_participation_by_contest_file(session.id, &contest.location, req_contest) {
return Some(true);
}
}
Some(false)
}
fn check_contest_constraints(session: &SessionUser, contest: &Contest) -> ContestStartConstraints { fn check_contest_constraints(session: &SessionUser, contest: &Contest) -> ContestStartConstraints {
let now = time::get_time(); let now = time::get_time();
let student_grade = session.grade % 100 - if session.grade / 100 == 1 { 1 } else { 0 }; let student_grade = session.grade % 100 - if session.grade / 100 == 1 { 1 } else { 0 };
...@@ -305,7 +317,8 @@ fn check_contest_constraints(session: &SessionUser, contest: &Contest) -> Contes ...@@ -305,7 +317,8 @@ fn check_contest_constraints(session: &SessionUser, contest: &Contest) -> Contes
contest_running, contest_running,
grade_too_low, grade_too_low,
grade_too_high, grade_too_high,
grade_matching } grade_matching,
}
} }
pub fn show_contest<T: MedalConnection>(conn: &T, contest_id: i32, session_token: &str, pub fn show_contest<T: MedalConnection>(conn: &T, contest_id: i32, session_token: &str,
...@@ -333,11 +346,13 @@ pub fn show_contest<T: MedalConnection>(conn: &T, contest_id: i32, session_token ...@@ -333,11 +346,13 @@ pub fn show_contest<T: MedalConnection>(conn: &T, contest_id: i32, session_token
fill_oauth_data(login_info, &mut data); fill_oauth_data(login_info, &mut data);
let constraints = check_contest_constraints(&session, &contest); let constraints = check_contest_constraints(&session, &contest);
let is_qualified = check_contest_qualification(conn, &session, &contest).unwrap_or(true);
let can_start = session.is_logged_in() && constraints.contest_running && constraints.grade_matching; let can_start = session.is_logged_in() && constraints.contest_running && constraints.grade_matching && is_qualified;
let has_duration = contest.duration > 0; let has_duration = contest.duration > 0;
data.insert("constraints".to_string(), to_json(&constraints)); data.insert("constraints".to_string(), to_json(&constraints));
data.insert("is_qualified".to_string(), to_json(&is_qualified));
data.insert("has_duration".to_string(), to_json(&has_duration)); data.insert("has_duration".to_string(), to_json(&has_duration));
data.insert("can_start".to_string(), to_json(&can_start)); data.insert("can_start".to_string(), to_json(&can_start));
...@@ -516,6 +531,12 @@ pub fn start_contest<T: MedalConnection>(conn: &T, contest_id: i32, session_toke ...@@ -516,6 +531,12 @@ pub fn start_contest<T: MedalConnection>(conn: &T, contest_id: i32, session_toke
return Err(MedalError::AccessDenied); return Err(MedalError::AccessDenied);
} }
let is_qualified = check_contest_qualification(conn, &session, &contest);
if is_qualified == Some(false) {
return Err(MedalError::AccessDenied);
}
// Start contest // Start contest
match conn.new_participation(&session_token, contest_id) { match conn.new_participation(&session_token, contest_id) {
Ok(_) => Ok(()), Ok(_) => Ok(()),
...@@ -587,8 +608,7 @@ pub fn signup<T: MedalConnection>(conn: &T, session_token: Option<String>, signu ...@@ -587,8 +608,7 @@ pub fn signup<T: MedalConnection>(conn: &T, session_token: Option<String>, signu
pub fn signupdata(query_string: Option<String>) -> json_val::Map<String, json_val::Value> { pub fn signupdata(query_string: Option<String>) -> json_val::Map<String, json_val::Value> {
let mut data = json_val::Map::new(); let mut data = json_val::Map::new();
if let Some(query) = query_string { if let Some(query) = query_string {
if query.starts_with("status=") { if let Some(status) = query.strip_prefix("status=") {
let status: &str = &query[7..];
if ["EmailTaken", "UsernameTaken", "UserLoggedIn", "EmptyFields"].contains(&status) { if ["EmailTaken", "UsernameTaken", "UserLoggedIn", "EmptyFields"].contains(&status) {
data.insert((status).to_string(), to_json(&true)); data.insert((status).to_string(), to_json(&true));
} }
...@@ -912,6 +932,14 @@ pub fn upload_groups<T: MedalConnection>(conn: &T, session_token: &str, csrf_tok ...@@ -912,6 +932,14 @@ pub fn upload_groups<T: MedalConnection>(conn: &T, session_token: &str, csrf_tok
user.firstname = Some(line[2].clone()); user.firstname = Some(line[2].clone());
user.lastname = Some(line[3].clone()); user.lastname = Some(line[3].clone());
use db_objects::Sex;
match line[4].as_str() {
"m" => user.sex = Some(Sex::Male as i32),
"f" => user.sex = Some(Sex::Female as i32),
"d" => user.sex = Some(Sex::Diverse as i32),
_ => user.sex = None,
}
group.members.push(user); group.members.push(user);
} }
conn.create_group_with_users(group); conn.create_group_with_users(group);
...@@ -978,8 +1006,7 @@ pub fn show_profile<T: MedalConnection>(conn: &T, session_token: &str, user_id: ...@@ -978,8 +1006,7 @@ pub fn show_profile<T: MedalConnection>(conn: &T, session_token: &str, user_id:
data.insert("ownprofile".into(), to_json(&true)); data.insert("ownprofile".into(), to_json(&true));
if let Some(query) = query_string { if let Some(query) = query_string {
if query.starts_with("status=") { if let Some(status) = query.strip_prefix("status=") {
let status: &str = &query[7..];
if ["NothingChanged", if ["NothingChanged",
"DataChanged", "DataChanged",
"PasswordChanged", "PasswordChanged",
...@@ -1043,8 +1070,7 @@ pub fn show_profile<T: MedalConnection>(conn: &T, session_token: &str, user_id: ...@@ -1043,8 +1070,7 @@ pub fn show_profile<T: MedalConnection>(conn: &T, session_token: &str, user_id:
data.insert("ownprofile".into(), to_json(&false)); data.insert("ownprofile".into(), to_json(&false));
if let Some(query) = query_string { if let Some(query) = query_string {
if query.starts_with("status=") { if let Some(status) = query.strip_prefix("status=") {
let status: &str = &query[7..];
if ["NothingChanged", "DataChanged", "PasswordChanged", "PasswordMissmatch"].contains(&status) { if ["NothingChanged", "DataChanged", "PasswordChanged", "PasswordMissmatch"].contains(&status) {
data.insert((status).to_string(), to_json(&true)); data.insert((status).to_string(), to_json(&true));
} }
...@@ -1063,8 +1089,8 @@ pub enum ProfileStatus { ...@@ -1063,8 +1089,8 @@ pub enum ProfileStatus {
PasswordChanged, PasswordChanged,
PasswordMissmatch, PasswordMissmatch,
} }
impl std::convert::Into<String> for ProfileStatus { impl From<ProfileStatus> for String {
fn into(self) -> String { format!("{:?}", self) } fn from(s: ProfileStatus) -> String { format!("{:?}", s) }
} }
pub fn edit_profile<T: MedalConnection>(conn: &T, session_token: &str, user_id: Option<i32>, csrf_token: &str, pub fn edit_profile<T: MedalConnection>(conn: &T, session_token: &str, user_id: Option<i32>, csrf_token: &str,
...@@ -1482,6 +1508,49 @@ pub fn admin_contest_export<T: MedalConnection>(conn: &T, contest_id: i32, sessi ...@@ -1482,6 +1508,49 @@ pub fn admin_contest_export<T: MedalConnection>(conn: &T, contest_id: i32, sessi
Ok(filename) Ok(filename)
} }
pub fn admin_show_cleanup<T: MedalConnection>(conn: &T, session_token: &str) -> MedalValueResult {
let session = conn.get_session(&session_token)
.ensure_logged_in()
.ok_or(MedalError::NotLoggedIn)?
.ensure_admin()
.ok_or(MedalError::AccessDenied)?;
let mut data = json_val::Map::new();
data.insert("csrf_token".to_string(), to_json(&session.csrf_token));
Ok(("admin_cleanup".to_string(), data))
}
pub fn admin_do_cleanup<T: MedalConnection>(conn: &T, session_token: &str, csrf_token: &str)
-> MedalValueResult {
let session = conn.get_session(&session_token)
.ensure_logged_in()
.ok_or(MedalError::NotLoggedIn)?
.ensure_admin()
.ok_or(MedalError::AccessDenied)?;
if session.csrf_token != csrf_token {
return Err(MedalError::CsrfCheckFailed);
}
let now = time::get_time();
let maxstudentage = now - time::Duration::days(180); // Delete managed users after 180 days of inactivity
let maxteacherage = now - time::Duration::days(1095); // Delete teachers after 3 years of inactivity
let maxage = now - time::Duration::days(3650); // Delete every user after 10 years of inactivity
let result = conn.remove_old_users_and_groups(maxstudentage, Some(maxteacherage), Some(maxage));
let mut data = json_val::Map::new();
if let Ok((n_users, n_groups, n_teachers, n_other)) = result {
let infodata = format!(",\"n_users\":{},\"n_groups\":{},\"n_teachers\":{},\"n_other\":{}",
n_users, n_groups, n_teachers, n_other);
data.insert("data".to_string(), to_json(&infodata));
Ok(("delete_ok".to_string(), data))
} else {
data.insert("reason".to_string(), to_json(&"Fehler."));
Ok(("delete_fail".to_string(), data))
}
}
#[derive(PartialEq, Clone, Copy)] #[derive(PartialEq, Clone, Copy)]
pub enum UserType { pub enum UserType {
User, User,
......
This diff is collapsed.
...@@ -44,6 +44,8 @@ pub trait MedalConnection { ...@@ -44,6 +44,8 @@ pub trait MedalConnection {
/// ///
/// Returns the `SessionUser` of the session. /// Returns the `SessionUser` of the session.
fn new_session(&self, key: &str) -> SessionUser; fn new_session(&self, key: &str) -> SessionUser;
/// Set activity date (for testing purposes)
fn session_set_activity_dates(&self, session_id: i32, account_created: Option<time::Timespec>, last_login: Option<time::Timespec>, last_activity: Option<time::Timespec>);
/// Saves the session data of `session` in the database. /// Saves the session data of `session` in the database.
fn save_session(&self, session: SessionUser); fn save_session(&self, session: SessionUser);
/// Combination of [`get_session`](#tymethod.get_session) and [`new_session`](#tymethod.new_session). /// Combination of [`get_session`](#tymethod.get_session) and [`new_session`](#tymethod.new_session).
...@@ -121,6 +123,8 @@ pub trait MedalConnection { ...@@ -121,6 +123,8 @@ pub trait MedalConnection {
/// Returns an `Vec` that contains pairs of all participations with their associated contests. /// Returns an `Vec` that contains pairs of all participations with their associated contests.
fn get_all_participations_complete(&self, session_id: i32) -> Vec<(Participation, Contest)>; fn get_all_participations_complete(&self, session_id: i32) -> Vec<(Participation, Contest)>;
fn has_participation_by_contest_file(&self, session_id: i32, location: &str, filename: &str) -> bool;
/// Start a new participation of the session identified by the session token `session` for the contest with the /// Start a new participation of the session identified by the session token `session` for the contest with the
/// contest id `contest_id`. It checks whether the session is allowed to start the participation. /// contest id `contest_id`. It checks whether the session is allowed to start the participation.
/// ///
...@@ -141,6 +145,7 @@ pub trait MedalConnection { ...@@ -141,6 +145,7 @@ pub trait MedalConnection {
fn delete_user(&self, user_id: i32); fn delete_user(&self, user_id: i32);
fn delete_group(&self, group_id: i32); fn delete_group(&self, group_id: i32);
fn delete_participation(&self, user_id: i32, contest_id: i32); fn delete_participation(&self, user_id: i32, contest_id: i32);
fn remove_old_users_and_groups(&self, maxstudentage: time::Timespec, maxteacherage: Option<time::Timespec>, maxage: Option<time::Timespec>) -> Result<(i32, i32, i32, i32),()>;
fn get_search_users(&self, fn get_search_users(&self,
_: (Option<i32>, _: (Option<i32>,
......
This diff is collapsed.
This diff is collapsed.
...@@ -23,7 +23,7 @@ pub struct SessionUser { ...@@ -23,7 +23,7 @@ pub struct SessionUser {
pub csrf_token: String, pub csrf_token: String,
pub last_login: Option<Timespec>, pub last_login: Option<Timespec>,
pub last_activity: Option<Timespec>, pub last_activity: Option<Timespec>,
pub permanent_login: bool, pub account_created: Option<Timespec>,
pub username: Option<String>, pub username: Option<String>,
pub password: Option<String>, pub password: Option<String>,
...@@ -54,6 +54,16 @@ pub struct SessionUser { ...@@ -54,6 +54,16 @@ pub struct SessionUser {
// pub pms_school_id: Option<i32>, // pub pms_school_id: Option<i32>,
} }
pub enum Sex {
#[allow(dead_code)]
NotStated = 0,
Male = 1,
Female = 2,
Diverse = 3,
#[allow(dead_code)]
Other = 4,
}
// Short version for display // Short version for display
#[derive(Clone, Default)] #[derive(Clone, Default)]
pub struct UserInfo { pub struct UserInfo {
...@@ -89,6 +99,7 @@ pub struct Contest { ...@@ -89,6 +99,7 @@ pub struct Contest {
pub max_grade: Option<i32>, pub max_grade: Option<i32>,
pub positionalnumber: Option<i32>, pub positionalnumber: Option<i32>,
pub requires_login: Option<bool>, pub requires_login: Option<bool>,
pub requires_contest: Option<String>,
pub secret: Option<String>, pub secret: Option<String>,
pub taskgroups: Vec<Taskgroup>, pub taskgroups: Vec<Taskgroup>,
pub message: Option<String>, pub message: Option<String>,
...@@ -169,7 +180,7 @@ impl Contest { ...@@ -169,7 +180,7 @@ impl Contest {
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub fn new(location: String, filename: String, name: String, duration: i32, public: bool, pub fn new(location: String, filename: String, name: String, duration: i32, public: bool,
start: Option<Timespec>, end: Option<Timespec>, min_grade: Option<i32>, max_grade: Option<i32>, start: Option<Timespec>, end: Option<Timespec>, min_grade: Option<i32>, max_grade: Option<i32>,
positionalnumber: Option<i32>, requires_login: Option<bool>, secret: Option<String>, positionalnumber: Option<i32>, requires_login: Option<bool>, requires_contest: Option<String>, secret: Option<String>,
message: Option<String>) message: Option<String>)
-> Self -> Self
{ {
...@@ -185,6 +196,7 @@ impl Contest { ...@@ -185,6 +196,7 @@ impl Contest {
max_grade, max_grade,
positionalnumber, positionalnumber,
requires_login, requires_login,
requires_contest,
secret, secret,
message, message,
taskgroups: Vec::new() } taskgroups: Vec::new() }
...@@ -198,9 +210,9 @@ impl SessionUser { ...@@ -198,9 +210,9 @@ impl SessionUser {
session_token: Some(session_token), session_token: Some(session_token),
csrf_token, csrf_token,
last_login: None, last_login: None,
last_activity: None, // now? last_activity: None,
account_created: Some(time::get_time()),
// müssen die überhaupt außerhalb der datenbankabstraktion sichtbar sein? // müssen die überhaupt außerhalb der datenbankabstraktion sichtbar sein?
permanent_login: false,
username: None, username: None,
password: None, password: None,
...@@ -238,7 +250,7 @@ impl SessionUser { ...@@ -238,7 +250,7 @@ impl SessionUser {
csrf_token: "".to_string(), csrf_token: "".to_string(),
last_login: None, last_login: None,
last_activity: None, last_activity: None,
permanent_login: false, account_created: Some(time::get_time()),
username: None, username: None,
password: None, password: None,
...@@ -266,7 +278,7 @@ impl SessionUser { ...@@ -266,7 +278,7 @@ impl SessionUser {
} }
pub fn is_alive(&self) -> bool { pub fn is_alive(&self) -> bool {
let duration = if self.permanent_login { Duration::days(90) } else { Duration::hours(9) }; let duration = Duration::hours(9); // TODO: hardcoded value, should be moved into constant or sth
let now = time::get_time(); let now = time::get_time();
if let Some(last_activity) = self.last_activity { if let Some(last_activity) = self.last_activity {
now - last_activity < duration now - last_activity < duration
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment