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
*.pdf
*.json
*.yaml
*.sql
*.sqlite
*.csv
/bulma
......@@ -1064,7 +1064,7 @@ checksum = "79c56d6a0b07f9e19282511c83fc5b086364cbae4ba8c7d5f190c3d9b0425a48"
[[package]]
name = "medal"
version = "1.6.0"
version = "1.7.0"
dependencies = [
"bcrypt",
"csv",
......
[package]
version = "1.6.0"
version = "1.7.0"
name = "medal"
authors = ["Robert Czechowski <czechowski@bwinf.de>", "Daniel Brüning <bruening@bwinf.de>"]
......
......@@ -20,7 +20,7 @@ format: src/db_conn_postgres.rs
cargo +nightly fmt
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
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 @@
* 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/>. */
use oauth_provider;
use std::path::{Path, PathBuf};
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)]
pub struct Config {
pub host: Option<String>,
pub port: Option<u16>,
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_url: Option<String>,
pub template: Option<String>,
......
......@@ -27,6 +27,7 @@ struct ContestYaml {
public_listing: Option<bool>,
requires_login: Option<bool>,
requires_contest: Option<Vec<String>>,
secret: Option<String>,
message: Option<String>,
......@@ -62,6 +63,7 @@ pub fn parse_yaml(content: &str, filename: &str, directory: &str) -> Option<Cont
config.max_grade,
config.position,
config.requires_login,
config.requires_contest.map(|list| list.join(",")),
config.secret,
config.message);
// TODO: Timeparsing should fail more pleasantly (-> Panic, thus shows message)
......
......@@ -21,7 +21,7 @@ use db_objects::OptionSession;
use db_objects::SessionUser;
use db_objects::{Contest, Grade, Group, Participation, Submission, Taskgroup};
use helpers;
use oauth_provider::OauthProvider;
use config::OauthProvider;
use webfw_iron::{json_val, to_json};
#[derive(Serialize, Deserialize)]
......@@ -286,6 +286,18 @@ pub struct ContestStartConstraints {
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 {
let now = time::get_time();
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
contest_running,
grade_too_low,
grade_too_high,
grade_matching }
grade_matching,
}
}
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
fill_oauth_data(login_info, &mut data);
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;
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("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
return Err(MedalError::AccessDenied);
}
let is_qualified = check_contest_qualification(conn, &session, &contest);
if is_qualified == Some(false) {
return Err(MedalError::AccessDenied);
}
// Start contest
match conn.new_participation(&session_token, contest_id) {
Ok(_) => Ok(()),
......@@ -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> {
let mut data = json_val::Map::new();
if let Some(query) = query_string {
if query.starts_with("status=") {
let status: &str = &query[7..];
if let Some(status) = query.strip_prefix("status=") {
if ["EmailTaken", "UsernameTaken", "UserLoggedIn", "EmptyFields"].contains(&status) {
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
user.firstname = Some(line[2].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);
}
conn.create_group_with_users(group);
......@@ -978,8 +1006,7 @@ pub fn show_profile<T: MedalConnection>(conn: &T, session_token: &str, user_id:
data.insert("ownprofile".into(), to_json(&true));
if let Some(query) = query_string {
if query.starts_with("status=") {
let status: &str = &query[7..];
if let Some(status) = query.strip_prefix("status=") {
if ["NothingChanged",
"DataChanged",
"PasswordChanged",
......@@ -1043,8 +1070,7 @@ pub fn show_profile<T: MedalConnection>(conn: &T, session_token: &str, user_id:
data.insert("ownprofile".into(), to_json(&false));
if let Some(query) = query_string {
if query.starts_with("status=") {
let status: &str = &query[7..];
if let Some(status) = query.strip_prefix("status=") {
if ["NothingChanged", "DataChanged", "PasswordChanged", "PasswordMissmatch"].contains(&status) {
data.insert((status).to_string(), to_json(&true));
}
......@@ -1063,8 +1089,8 @@ pub enum ProfileStatus {
PasswordChanged,
PasswordMissmatch,
}
impl std::convert::Into<String> for ProfileStatus {
fn into(self) -> String { format!("{:?}", self) }
impl From<ProfileStatus> for String {
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,
......@@ -1482,6 +1508,49 @@ pub fn admin_contest_export<T: MedalConnection>(conn: &T, contest_id: i32, sessi
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)]
pub enum UserType {
User,
......
This diff is collapsed.
......@@ -44,6 +44,8 @@ pub trait MedalConnection {
///
/// Returns the `SessionUser` of the session.
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.
fn save_session(&self, session: SessionUser);
/// Combination of [`get_session`](#tymethod.get_session) and [`new_session`](#tymethod.new_session).
......@@ -121,6 +123,8 @@ pub trait MedalConnection {
/// 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 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
/// contest id `contest_id`. It checks whether the session is allowed to start the participation.
///
......@@ -141,6 +145,7 @@ pub trait MedalConnection {
fn delete_user(&self, user_id: i32);
fn delete_group(&self, group_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,
_: (Option<i32>,
......
This diff is collapsed.
This diff is collapsed.
......@@ -23,7 +23,7 @@ pub struct SessionUser {
pub csrf_token: String,
pub last_login: Option<Timespec>,
pub last_activity: Option<Timespec>,
pub permanent_login: bool,
pub account_created: Option<Timespec>,
pub username: Option<String>,
pub password: Option<String>,
......@@ -54,6 +54,16 @@ pub struct SessionUser {
// 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
#[derive(Clone, Default)]
pub struct UserInfo {
......@@ -89,6 +99,7 @@ pub struct Contest {
pub max_grade: Option<i32>,
pub positionalnumber: Option<i32>,
pub requires_login: Option<bool>,
pub requires_contest: Option<String>,
pub secret: Option<String>,
pub taskgroups: Vec<Taskgroup>,
pub message: Option<String>,
......@@ -169,7 +180,7 @@ impl Contest {
#[allow(clippy::too_many_arguments)]
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>,
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>)
-> Self
{
......@@ -185,6 +196,7 @@ impl Contest {
max_grade,
positionalnumber,
requires_login,
requires_contest,
secret,
message,
taskgroups: Vec::new() }
......@@ -198,9 +210,9 @@ impl SessionUser {
session_token: Some(session_token),
csrf_token,
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?
permanent_login: false,
username: None,
password: None,
......@@ -238,7 +250,7 @@ impl SessionUser {
csrf_token: "".to_string(),
last_login: None,
last_activity: None,
permanent_login: false,
account_created: Some(time::get_time()),
username: None,
password: None,
......@@ -266,7 +278,7 @@ impl SessionUser {
}
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();
if let Some(last_activity) = self.last_activity {
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