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

Merge branch 'v1.6' into deploy

parents 837a494b 68d2b546
Pipeline #915 passed with stages
in 10 minutes and 27 seconds
......@@ -4,4 +4,16 @@ config.json
**/*.rs.bk
*~
*.db
/export
/gitstats
/tasks/*
**/.directory
**/#*#
/.idea
*.pdf
*.json
*.yaml
*.sql
*.sqlite
*.csv
/bulma
......@@ -1064,7 +1064,7 @@ checksum = "79c56d6a0b07f9e19282511c83fc5b086364cbae4ba8c7d5f190c3d9b0425a48"
[[package]]
name = "medal"
version = "1.5.3"
version = "1.6.0"
dependencies = [
"bcrypt",
"csv",
......
[package]
version = "1.5.3"
version = "1.6.0"
name = "medal"
authors = ["Robert Czechowski <czechowski@bwinf.de>", "Daniel Brüning <bruening@bwinf.de>"]
......
# Configuration
* `database_file`:
* `host`
* `port`:
* `template`:
* `self_url`:
* `cookie_signing_secret`:
* `require_sex`:
* `allow_sex_na`:
* `allow_sex_diverse`:
* `allow_sex_other`:
* `server_message`:
* `oauth_providers`:
\ No newline at end of file
# The Medal Contest Platform
# The Medal Contest Platform ![Logo](static/images/medal_logo_small.png)
[![crates.io](https://img.shields.io/crates/v/medal?color=orange)](https://crates.io/crates/medal)
[![documentation](https://img.shields.io/crates/v/medal?label=docs)](https://jim.test.bwinf.de/doc/medal/)
......@@ -52,15 +52,15 @@ The `config.json` configures the plattform (see src/config.rs).
Needs `rustc` and `cargo` 1.40 (stable) or higher.
Rust can be obtained here: https://rustup.rs/
Rust can be obtained here: https://rustup.rs/
Running
Running
```
make
```
compiles and runs a debug-/test-server.
For production use, a release binary should be compiled and served behind a reverse proxy (nginx, apache, …).
For production use, a release binary should be compiled and served behind a reverse proxy (nginx, apache, …).
```
make release
```
......@@ -81,7 +81,7 @@ upstream medal {
server {
# Other server settings here
location ~* \.(yaml)$ {
deny all;
}
......@@ -92,7 +92,7 @@ server {
location /tasks {
add_header Cache-Control "public, max-age=604800";
}
}
location / {
proxy_pass http://medal;
......@@ -105,13 +105,13 @@ The following configuration can be used for an Apache 2.4 webserver:
ServerSignature Off
ProxyPreserveHost On
AllowEncodedSlashes NoDecode
ProxyPass /static/ !
ProxyPass /tasks/ !
ProxyPass /favicon.ico !
ProxyPass / http://[::1]:8080/
ProxyPassReverse / http://[::1]:8080/
Alias "/tasks/" "/path/to/medal/tasks/"
Alias "/static/" "/path/to/medal/static/"
Alias "/favicon.ico" "/path/to/medal/static/images/favicon.png"
......@@ -119,7 +119,7 @@ The following configuration can be used for an Apache 2.4 webserver:
<filesMatch "\.(css|jpe?g|png|gif|js|ico)$">
Header set Cache-Control "max-age=604800, public"
</filesMatch>
<FilesMatch "\.yaml$">
Deny from all
</FilesMatch>
......@@ -127,7 +127,7 @@ The following configuration can be used for an Apache 2.4 webserver:
<Directory "/path/to/medal/static/">
Require all granted
</Directory>
<Directory "/path/to/medal/tasks/">
Require all granted
</Directory>
......@@ -139,7 +139,7 @@ The following configuration can be used for an Apache 2.4 webserver:
Please format your code with `rustfmt` and check it for warnings with `clippy`.
You can install those with
You can install those with
```
rustup component add rustfmt --toolchain nightly
rustup component add clippy
......
{
"name": "JwInf 2018 Runde 1",
"participationStart": "2009-01-01T12:00:00+01:00",
"participationEnd": "2009-01-01T12:00:00+01:00",
"durationMinutes": 45,
"publicListing": true,
"tasks": {
"Aufgabe 1": "tasks/task1",
"Aufgabe 2": ["tasks/task2a", "tasks/task2b", "tasks/task3"]
"Aufgabe 3": {
"tasks/task4a": {"stars": 1},
"tasks/task4b": {"stars": 3},
"tasks/task3": {"stars": 5}
}
}
}
language: rust
rust:
- stable
- nightly
script:
- cargo build
- cargo test
- cargo build --features=redis-backend
- cargo test --features=redis-backend
cache:
directories:
- $HOME/.cargo
- target
- local
......@@ -32,8 +32,12 @@ pub struct Config {
pub cookie_signing_secret: Option<String>,
pub disable_results_page: Option<bool>,
pub enable_password_login: Option<bool>,
pub server_message: Option<String>,
pub teacher_page: Option<String>,
pub require_sex: Option<bool>,
pub allow_sex_na: Option<bool>,
pub allow_sex_diverse: Option<bool>,
pub allow_sex_other: Option<bool>,
pub dbstatus_secret: Option<String>,
pub template_params: Option<::std::collections::BTreeMap<String, String>>,
}
#[derive(StructOpt, Debug)]
......
......@@ -46,13 +46,15 @@ pub struct ContestInfo {
pub public: bool,
}
#[derive(Clone)]
#[derive(Clone, Debug)]
pub enum MedalError {
NotLoggedIn,
AccessDenied,
CsrfCheckFailed,
SessionTimeout,
DatabaseError,
ConfigurationError,
DatabaseConnectionError,
PasswordHashingError,
UnmatchedPasswords,
NotFound,
......@@ -80,6 +82,7 @@ fn fill_user_data(session: &SessionUser, data: &mut json_val::Map<String, serde_
data.insert("firstname".to_string(), to_json(&session.firstname));
data.insert("lastname".to_string(), to_json(&session.lastname));
data.insert("teacher".to_string(), to_json(&session.is_teacher));
data.insert("admin".to_string(), to_json(&session.is_admin));
data.insert("csrf_token".to_string(), to_json(&session.csrf_token));
data.insert("parent".to_string(), to_json(&"base"));
......@@ -149,7 +152,13 @@ pub fn show_login<T: MedalConnection>(conn: &T, session_token: Option<String>, l
("login".to_owned(), data)
}
pub fn status<T: MedalConnection>(conn: &T, _: ()) -> String { conn.get_debug_information() }
pub fn status<T: MedalConnection>(conn: &T, config_secret: Option<String>, given_secret: Option<String>) -> MedalResult<String> {
if config_secret == given_secret {
Ok(conn.get_debug_information())
} else {
Err(MedalError::AccessDenied)
}
}
pub fn debug<T: MedalConnection>(conn: &T, session_token: Option<String>)
-> (String, json_val::Map<String, json_val::Value>) {
......@@ -189,7 +198,7 @@ pub fn debug<T: MedalConnection>(conn: &T, session_token: Option<String>)
pub fn debug_create_session<T: MedalConnection>(conn: &T, session_token: Option<String>) {
if let Some(token) = session_token {
conn.get_session_or_new(&token);
conn.get_session_or_new(&token).unwrap();
}
}
......@@ -203,11 +212,11 @@ pub enum ContestVisibility {
pub fn show_contests<T: MedalConnection>(conn: &T, session_token: &str, login_info: LoginInfo,
visibility: ContestVisibility)
-> MedalValue
-> MedalValueResult
{
let mut data = json_val::Map::new();
let session = conn.get_session_or_new(&session_token);
let session = conn.get_session_or_new(&session_token).map_err(|_| MedalError::DatabaseConnectionError)?;
fill_user_data(&session, &mut data);
if session.is_logged_in() {
......@@ -242,7 +251,7 @@ pub fn show_contests<T: MedalConnection>(conn: &T, session_token: &str, login_in
ContestVisibility::All => "Alle Wettbewerbe",
}));
("contests".to_owned(), data)
Ok(("contests".to_owned(), data))
}
fn generate_subtaskstars(tg: &Taskgroup, grade: &Grade, ast: Option<i32>) -> Vec<SubTaskInfo> {
......@@ -303,7 +312,7 @@ pub fn show_contest<T: MedalConnection>(conn: &T, contest_id: i32, session_token
query_string: Option<String>, login_info: LoginInfo)
-> MedalValueResult
{
let session = conn.get_session_or_new(&session_token);
let session = conn.get_session_or_new(&session_token).unwrap();
let contest = conn.get_contest_by_id_complete(contest_id);
let grades = conn.get_contest_user_grades(&session_token, contest_id);
......@@ -319,6 +328,7 @@ pub fn show_contest<T: MedalConnection>(conn: &T, contest_id: i32, session_token
data.insert("parent".to_string(), to_json(&"base"));
data.insert("empty".to_string(), to_json(&"empty"));
data.insert("contest".to_string(), to_json(&ci));
data.insert("title".to_string(), to_json(&ci.name));
data.insert("message".to_string(), to_json(&contest.message));
fill_oauth_data(login_info, &mut data);
......@@ -417,6 +427,7 @@ pub fn show_contest<T: MedalConnection>(conn: &T, contest_id: i32, session_token
data.insert("time_left".to_string(), to_json(&time_left));
data.insert("seconds_left".to_string(), to_json(&left_secs));
}
data.insert("no_login".to_string(), to_json(&true));
}
// This only checks if a query string is existent, so any query string will
......@@ -485,7 +496,7 @@ pub fn show_contest_results<T: MedalConnection>(conn: &T, contest_id: i32, sessi
pub fn start_contest<T: MedalConnection>(conn: &T, contest_id: i32, session_token: &str, csrf_token: &str)
-> MedalResult<()> {
// TODO: Is _or_new the right semantic? We need a CSRF token anyway …
let session = conn.get_session_or_new(&session_token);
let session = conn.get_session_or_new(&session_token).unwrap();
let contest = conn.get_contest_by_id(contest_id);
// Check logged in or open contest
......@@ -680,7 +691,7 @@ pub fn save_submission<T: MedalConnection>(conn: &T, task_id: i32, session_token
}
pub fn show_task<T: MedalConnection>(conn: &T, task_id: i32, session_token: &str) -> MedalValueResult {
let session = conn.get_session_or_new(&session_token);
let session = conn.get_session_or_new(&session_token).unwrap();
let (t, tg, c) = conn.get_task_by_id_complete(task_id);
let grade = conn.get_taskgroup_user_grade(&session_token, tg.id.unwrap()); // TODO: Unwrap?
......@@ -736,6 +747,7 @@ pub fn show_task<T: MedalConnection>(conn: &T, task_id: i32, session_token: &str
data.insert("contestname".to_string(), to_json(&c.name));
data.insert("name".to_string(), to_json(&tg.name));
data.insert("title".to_string(), to_json(&format!("Aufgabe „{}“ in {}", &tg.name, &c.name)));
data.insert("taskid".to_string(), to_json(&task_id));
data.insert("csrf_token".to_string(), to_json(&session.csrf_token));
data.insert("taskpath".to_string(), to_json(&taskpath));
......@@ -918,8 +930,15 @@ pub fn show_groups_results<T: MedalConnection>(conn: &T, contest_id: i32, sessio
Ok(("groupresults".into(), data))
}
pub struct SexInformation {
pub require_sex: bool,
pub allow_sex_na: bool,
pub allow_sex_diverse: bool,
pub allow_sex_other: bool,
}
pub fn show_profile<T: MedalConnection>(conn: &T, session_token: &str, user_id: Option<i32>,
query_string: Option<String>)
query_string: Option<String>, sex_infos: SexInformation)
-> MedalValueResult
{
let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
......@@ -927,6 +946,11 @@ pub fn show_profile<T: MedalConnection>(conn: &T, session_token: &str, user_id:
let mut data = json_val::Map::new();
fill_user_data(&session, &mut data);
data.insert("require_sex".to_string(), to_json(&sex_infos.require_sex));
data.insert("allow_sex_na".to_string(), to_json(&sex_infos.allow_sex_na));
data.insert("allow_sex_diverse".to_string(), to_json(&sex_infos.allow_sex_diverse));
data.insert("allow_sex_other".to_string(), to_json(&sex_infos.allow_sex_other));
match user_id {
None => {
data.insert("profile_firstname".to_string(), to_json(&session.firstname));
......@@ -1161,8 +1185,7 @@ pub fn edit_profile<T: MedalConnection>(conn: &T, session_token: &str, user_id:
Ok(result)
}
pub fn teacher_infos<T: MedalConnection>(conn: &T, session_token: &str, teacher_page: Option<&str>)
-> MedalValueResult {
pub fn teacher_infos<T: MedalConnection>(conn: &T, session_token: &str) -> MedalValueResult {
let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
if !session.is_teacher {
return Err(MedalError::AccessDenied);
......@@ -1171,10 +1194,6 @@ pub fn teacher_infos<T: MedalConnection>(conn: &T, session_token: &str, teacher_
let mut data = json_val::Map::new();
fill_user_data(&session, &mut data);
if let Some(teacher_page) = teacher_page {
data.insert("teacher_page".to_string(), to_json(&teacher_page));
}
Ok(("teacher".to_string(), data))
}
......@@ -1234,6 +1253,8 @@ pub fn admin_show_user<T: MedalConnection>(conn: &T, user_id: i32, session_token
let mut data = json_val::Map::new();
let (user, opt_group) = conn.get_user_and_group_by_id(user_id).ok_or(MedalError::AccessDenied)?;
// TODO: This is not nice, the fill_user_data is meant to fill the data of the user being logged in right now!
// Need to find a better solution for this, so we have no longer to replace the CSRF token here
fill_user_data(&user, &mut data);
data.insert("logincode".to_string(), to_json(&user.logincode));
data.insert("userid".to_string(), to_json(&user.id));
......@@ -1254,7 +1275,6 @@ pub fn admin_show_user<T: MedalConnection>(conn: &T, user_id: i32, session_token
code: g.groupcode.clone() })
.collect();
data.insert("group".to_string(), to_json(&v));
data.insert("csrf_token".to_string(), to_json(&session.csrf_token));
let parts = conn.get_all_participations_complete(user_id);
......@@ -1262,6 +1282,7 @@ pub fn admin_show_user<T: MedalConnection>(conn: &T, user_id: i32, session_token
data.insert("participations".to_string(), to_json(&pi));
data.insert("csrf_token".to_string(), to_json(&session.csrf_token));
Ok(("admin_user".to_string(), data))
}
......@@ -1294,11 +1315,11 @@ pub fn admin_delete_user<T: MedalConnection>(conn: &T, user_id: i32, session_tok
}
pub fn admin_show_group<T: MedalConnection>(conn: &T, group_id: i32, session_token: &str) -> MedalValueResult {
conn.get_session(&session_token)
.ensure_logged_in()
.ok_or(MedalError::NotLoggedIn)?
.ensure_admin()
.ok_or(MedalError::AccessDenied)?;
let session = conn.get_session(&session_token)
.ensure_logged_in()
.ok_or(MedalError::NotLoggedIn)?
.ensure_admin()
.ok_or(MedalError::AccessDenied)?;
let group = conn.get_group_complete(group_id).unwrap(); // TODO handle error
......@@ -1328,6 +1349,7 @@ pub fn admin_show_group<T: MedalConnection>(conn: &T, group_id: i32, session_tok
data.insert("group_admin_firstname".to_string(), to_json(&user.firstname));
data.insert("group_admin_lastname".to_string(), to_json(&user.lastname));
data.insert("csrf_token".to_string(), to_json(&session.csrf_token));
Ok(("admin_group".to_string(), data))
}
......@@ -1357,11 +1379,11 @@ pub fn admin_delete_group<T: MedalConnection>(conn: &T, group_id: i32, session_t
pub fn admin_show_participation<T: MedalConnection>(conn: &T, user_id: i32, contest_id: i32, session_token: &str)
-> MedalValueResult {
conn.get_session(&session_token)
.ensure_logged_in()
.ok_or(MedalError::NotLoggedIn)?
.ensure_admin()
.ok_or(MedalError::AccessDenied)?;
let session = conn.get_session(&session_token)
.ensure_logged_in()
.ok_or(MedalError::NotLoggedIn)?
.ensure_admin()
.ok_or(MedalError::AccessDenied)?;
let contest = conn.get_contest_by_id_complete(contest_id);
......@@ -1395,6 +1417,7 @@ pub fn admin_show_participation<T: MedalConnection>(conn: &T, user_id: i32, cont
data.insert("start_date".to_string(),
to_json(&self::time::strftime("%FT%T%z", &self::time::at(participation.start)).unwrap()));
data.insert("csrf_token".to_string(), to_json(&session.csrf_token));
Ok(("admin_participation".to_string(), data))
}
......
......@@ -175,6 +175,8 @@ impl MedalObject<Connection> for Contest {
}
impl MedalConnection for Connection {
fn reconnect(config: &config::Config) -> Self { Self::reconnect_concrete(config) }
fn dbtype(&self) -> &'static str { "postgres" }
fn migration_already_applied(&self, name: &str) -> bool {
......@@ -308,16 +310,21 @@ impl MedalConnection for Connection {
SessionUser::minimal(id, session_token.to_owned(), csrf_token)
}
fn get_session_or_new(&self, key: &str) -> SessionUser {
let query = "UPDATE session
SET session_token = $1
WHERE session_token = $2";
self.get_session(&key).ensure_alive().unwrap_or_else(|| {
// TODO: Factor this out in own function
// TODO: Should a new session key be generated every time?
self.execute(query, &[&Option::<String>::None, &key]).unwrap();
self.new_session(&key)
})
fn get_session_or_new(&self, key: &str) -> Result<SessionUser, ()> {
fn disable_old_session_and_create_new(conn: &Connection, key: &str) -> Result<SessionUser, ()> {
let query = "UPDATE session
SET session_token = $1
WHERE session_token = $2";
// TODO: Should a new session key be generated every time?
conn.execute(query, &[&Option::<String>::None, &key]).map_err(|_| ())?;
Ok(conn.new_session(&key))
}
if let Some(session) = self.get_session(&key).ensure_alive() {
Ok(session)
} else {
disable_old_session_and_create_new(self, key)
}
}
fn get_user_by_id(&self, user_id: i32) -> Option<SessionUser> {
......@@ -579,7 +586,7 @@ impl MedalConnection for Connection {
fn signup(&self, session_token: &str, username: &str, email: &str, password_hash: String, salt: &str)
-> SignupResult {
let mut session_user = self.get_session_or_new(&session_token);
let mut session_user = self.get_session_or_new(&session_token).unwrap();
if session_user.is_logged_in() {
return SignupResult::UserLoggedIn;
......@@ -874,6 +881,7 @@ impl MedalConnection for Connection {
"teacher_firstname",
"teacher_lastname",
"teacher_oauth_foreign_id",
"teacher_oauth_school_id",
"teacher_oauth_provider",
"contest_id",
"start_date"];
......@@ -934,6 +942,17 @@ impl MedalConnection for Connection {
for i in 20..20 + taskgroups.len() {
points.push(row.get::<_, Option<i32>>(i));
}
let teacher_oauth_and_school_id = row.get::<_, Option<String>>(15);
let (teacher_oauth_id, teacher_school_id) = if let Some(toasi) = teacher_oauth_and_school_id {
let mut v = toasi.split('/');
let oid: Option<String> = v.next().map(|s| s.to_owned());
let sid: Option<String> = v.next().map(|s| s.to_owned());
(oid, sid)
} else {
(None, None)
};
// Serialized as several tuples because Serde only supports tuples up to a certain length
// (16 according to https://docs.serde.rs/serde/trait.Deserialize.html)
wtr.serialize(((row.get::<_, i32>(0),
......@@ -952,7 +971,8 @@ impl MedalConnection for Connection {
row.get::<_, Option<i32>>(13),
row.get::<_, Option<String>>(14),
row.get::<_, Option<String>>(15),
row.get::<_, Option<String>>(16),
teacher_oauth_id,
teacher_school_id,
row.get::<_, Option<String>>(17)),
row.get::<_, Option<i32>>(18),
row.get::<_, Option<self::time::Timespec>>(19)
......@@ -1416,13 +1436,14 @@ impl MedalConnection for Connection {
let query = "SELECT id, firstname, lastname
FROM session
WHERE oauth_foreign_id = $1
OR oauth_foreign_id LIKE $2
LIMIT 30";
Ok(self.query_map_many(query, &[&pms_id], |row| (row.get(0), row.get(1), row.get(2))).unwrap())
Ok(self.query_map_many(query, &[&pms_id, &format!("{}/%", pms_id)], |row| (row.get(0), row.get(1), row.get(2))).unwrap())
} else if let (Some(firstname), Some(lastname)) = (s_firstname, s_lastname) {
let query = "SELECT id, firstname, lastname
FROM session
WHERE firstname LIKE $1
AND lastname LIKE $2
WHERE firstname ILIKE $1
AND lastname ILIKE $2
LIMIT 30";
Ok(self.query_map_many(query, &[&firstname, &lastname], |row| (row.get(0), row.get(1), row.get(2)))
.unwrap())
......
......@@ -12,6 +12,7 @@
* 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 config;
use db_objects::*;
#[derive(Debug)]
......@@ -26,6 +27,8 @@ pub enum SignupResult {
/// This trait abstracts the database connection and provides function for all actions to be performed on the database
/// in the medal platform.
pub trait MedalConnection {
fn reconnect(&config::Config) -> Self;
fn dbtype(&self) -> &'static str;
fn migration_already_applied(&self, name: &str) -> bool;
......@@ -44,7 +47,9 @@ pub trait MedalConnection {
/// 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).
fn get_session_or_new(&self, key: &str) -> SessionUser;
///
/// This method can still fail in case of database error in order to bubble them up to the webframework
fn get_session_or_new(&self, key: &str) -> Result<SessionUser, ()>;
/// Try to get session associated to the id `user_id`.
///
......
......@@ -16,6 +16,7 @@
extern crate postgres;
use config;
use postgres::Connection;
use time;
use time::Duration;
......@@ -34,6 +35,8 @@ trait Queryable {
where F: FnMut(postgres::rows::Row<'_>) -> T;
fn exists(&self, sql: &str, params: &[&dyn postgres::types::ToSql]) -> bool;
fn get_last_id(&self) -> Option<i32>;
fn reconnect_concrete(&config::Config) -> Self;
}
impl Queryable for Connection {
......@@ -62,6 +65,10 @@ impl Queryable for Connection {
})
}
// Empty line intended
fn reconnect_concrete(config: &config::Config) -> Self {
postgres::Connection::connect(config.database_url.clone().unwrap(), postgres::TlsMode::None).unwrap()
}
}
impl MedalObject<Connection> for Submission {
......
......@@ -29,6 +29,7 @@
extern crate postgres;
use config;
use postgres::Connection;
use time;
use time::Duration;
......@@ -47,6 +48,8 @@ trait Queryable {
where F: FnMut(postgres::rows::Row<'_>) -> T;
fn exists(&self, sql: &str, params: &[&dyn postgres::types::ToSql]) -> bool;
fn get_last_id(&self) -> Option<i32>;
fn reconnect_concrete(&config::Config) -> Self;
}
impl Queryable for Connection {
......@@ -75,6 +78,10 @@ impl Queryable for Connection {
})
}
// Empty line intended
fn reconnect_concrete(config: &config::Config) -> Self {
postgres::Connection::connect(config.database_url.clone().unwrap(), postgres::TlsMode::None).unwrap()
}
}
impl MedalObject<Connection> for Submission {
......@@ -287,6 +294,8 @@ impl MedalObject<Connection> for Contest {
}
impl MedalConnection for Connection {
fn reconnect(config: &config::Config) -> Self { Self::reconnect_concrete(config) }
fn dbtype(&self) -> &'static str { "postgres" }
fn migration_already_applied(&self, name: &str) -> bool {