medal/
config.rs

1/*  medal                                                                                                            *\
2 *  Copyright (C) 2022  Bundesweite Informatikwettbewerbe, Robert Czechowski                                         *
3 *                                                                                                                   *
4 *  This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero        *
5 *  General Public License as published  by the Free Software Foundation, either version 3 of the License, or (at    *
6 *  your option) any later version.                                                                                  *
7 *                                                                                                                   *
8 *  This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the       *
9 *  implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public      *
10 *  License for more details.                                                                                        *
11 *                                                                                                                   *
12 *  You should have received a copy of the GNU Affero General Public License along with this program.  If not, see   *
13\*  <http://www.gnu.org/licenses/>.                                                                                  */
14
15use std::path::{Path, PathBuf};
16
17use structopt::StructOpt;
18
19#[derive(Serialize, Deserialize, Clone, Default, Debug)]
20pub struct OauthProvider {
21    pub provider_id: String,
22    pub medal_oauth_type: String,
23    pub url: String,
24    pub client_id: String,
25    pub client_secret: String,
26    pub access_token_url: String,
27    pub user_data_url: String,
28    pub school_data_url: Option<String>,
29    pub school_data_secret: Option<String>,
30    pub allow_teacher_login_without_school: Option<bool>,
31    pub login_link_text: String,
32}
33
34#[derive(Serialize, Deserialize, Clone, Default, Debug)]
35pub struct Config {
36    pub host: Option<String>,
37    pub port: Option<u16>,
38    pub self_url: Option<String>,
39    pub oauth_providers: Option<Vec<OauthProvider>>,
40    pub database_file: Option<PathBuf>,
41    pub database_url: Option<String>,
42    pub template: Option<String>,
43    pub no_contest_scan: Option<bool>,
44    pub open_browser: Option<bool>,
45    pub cookie_signing_secret: Option<String>,
46    pub disable_results_page: Option<bool>,
47    pub enable_password_login: Option<bool>,
48    pub require_sex: Option<bool>,
49    pub allow_sex_na: Option<bool>,
50    pub allow_sex_diverse: Option<bool>,
51    pub allow_sex_other: Option<bool>,
52    pub dbstatus_secret: Option<String>,
53    pub template_params: Option<::std::collections::BTreeMap<String, serde_json::Value>>,
54    pub only_contest_scan: Option<bool>,
55    pub reset_admin_pw: Option<bool>,
56    pub log_timing: Option<bool>,
57    pub auto_save_interval: Option<u64>,
58    pub version: Option<bool>,
59    pub restricted_task_directories: Option<Vec<String>>,
60    pub autoclean_submissions: Option<bool>,
61}
62
63#[derive(StructOpt, Debug)]
64#[structopt()]
65struct Opt {
66    /// Config file to use (default: 'config.json')
67    #[structopt(short = "c", long = "config", default_value = "config.yaml", parse(from_os_str))]
68    pub configfile: PathBuf,
69
70    /// Database file to use (default: from config file or 'medal.db')
71    #[structopt(short = "d", long = "database", parse(from_os_str))]
72    pub databasefile: Option<PathBuf>,
73
74    /// Database file to use (default: from config file or 'medal.db')
75    #[structopt(short = "D", long = "databaseurl")]
76    pub databaseurl: Option<String>,
77
78    /// Port to listen on (default: from config file or 8080)
79    #[structopt(short = "p", long = "port")]
80    pub port: Option<u16>,
81
82    /// Teacher page in task directory
83    #[structopt(short = "t", long = "template")]
84    pub template: Option<String>,
85
86    /// Reset password of admin user (user_id=1)
87    #[structopt(short = "a", long = "reset-admin-pw")]
88    pub resetadminpw: bool,
89
90    /// Run medal without scanning for contests
91    #[structopt(short = "S", long = "no-contest-scan")]
92    pub nocontestscan: bool,
93
94    /// Scan for contests without starting medal
95    #[structopt(short = "s", long = "only-contest-scan")]
96    pub onlycontestscan: bool,
97
98    /// Automatically open medal in the default browser
99    #[structopt(short = "b", long = "browser")]
100    pub openbrowser: bool,
101
102    /// Disable results page to reduce load on the server
103    #[structopt(long = "disable-results-page")]
104    pub disableresultspage: bool,
105
106    /// Enable the login with username and password
107    #[structopt(short = "P", long = "passwordlogin")]
108    pub enablepasswordlogin: bool,
109
110    /// Teacher page in task directory
111    #[structopt(short = "T", long = "teacherpage")]
112    pub teacherpage: Option<String>,
113
114    /// Log response time of every request
115    #[structopt(long = "log-timing")]
116    pub logtiming: bool,
117
118    /// Auto save interval in seconds (defaults to 10)
119    #[structopt(long = "auto-save-interval")]
120    pub autosaveinterval: Option<u64>,
121
122    /// Show version
123    #[structopt(long = "version")]
124    pub version: bool,
125}
126
127enum FileType {
128    Json,
129    Yaml,
130}
131
132pub fn read_config_from_file(file: &Path) -> Config {
133    use std::io::Read;
134
135    let file_type = match file.extension().map(|e| e.to_str().unwrap_or("<Encoding error>")) {
136        Some("yaml") | Some("YAML") => FileType::Yaml,
137        Some("json") | Some("JSON") => FileType::Json,
138        Some(ext) => panic!("Config file has unknown file extension `{}` (supported types are YAML and JSON).", ext),
139        None => panic!("Config file has no file extension (supported types are YAML and JSON)."),
140    };
141
142    println!("Reading configuration file '{}'", file.to_str().unwrap_or("<Encoding error>"));
143
144    let mut config: Config = if let Ok(mut file) = std::fs::File::open(file) {
145        let mut contents = String::new();
146        file.read_to_string(&mut contents).unwrap();
147        match file_type {
148            FileType::Json => serde_json::from_str(&contents).unwrap(),
149            FileType::Yaml => serde_yaml::from_str(&contents).unwrap(),
150        }
151    } else {
152        println!("Configuration file '{}' not found. Using default configuration.",
153                 file.to_str().unwrap_or("<Encoding error>"));
154        Default::default()
155    };
156
157    if let Some(ref rtds) = config.restricted_task_directories {
158        // Some sanity checks
159        for rtd in rtds {
160            if !Path::new(rtd).exists() {
161                println!("WARNING: restricted task directory '{}' does NOT exist!", rtd);
162            }
163            if rtd.chars().last().unwrap_or(' ') != '/' {
164                println!("WARNING: restricted task directory '{}' does NOT end with a '/'", rtd);
165            }
166            if !rtd.starts_with("tasks/") {
167                println!("WARNING: restricted task directory '{}' does NOT start with 'tasks/'", rtd);
168            }
169            if rtd == "tasks/" {
170                println!("WARNING: restricted task directory '{}' restricts ALL tasks", rtd);
171            }
172        }
173    }
174
175    if let Some(ref oap) = config.oauth_providers {
176        println!("OAuth providers:");
177        for oap in oap {
178            println!("  * {}", oap.provider_id);
179        }
180    }
181
182    if config.host.is_none() {
183        config.host = Some("[::]".to_string())
184    }
185    if config.port.is_none() {
186        config.port = Some(8080)
187    }
188    if config.self_url.is_none() {
189        config.self_url = Some("http://localhost:8080".to_string())
190    }
191    if config.template.is_none() {
192        config.template = Some("default".to_string())
193    }
194    if config.no_contest_scan.is_none() {
195        config.no_contest_scan = Some(false)
196    }
197    if config.open_browser.is_none() {
198        config.open_browser = Some(false)
199    }
200    if config.enable_password_login.is_none() {
201        config.enable_password_login = Some(false)
202    }
203    if config.auto_save_interval.is_none() {
204        config.auto_save_interval = Some(10)
205    }
206
207    println!("OAuth providers will be told to redirect to {}", config.self_url.as_ref().unwrap());
208
209    config
210}
211
212fn merge_value<T>(into: &mut Option<T>, from: Option<T>) { from.map(|x| *into = Some(x)); }
213
214fn merge_flag(into: &mut Option<bool>, from: bool) {
215    if from {
216        *into = Some(true);
217    }
218}
219
220pub fn get_config() -> Config {
221    let opt = Opt::from_args();
222    if opt.version {
223        return Config { version: Some(true), ..Config::default() };
224    }
225
226    #[cfg(feature = "debug")]
227    println!("Options: {:#?}", opt);
228
229    let mut config = read_config_from_file(&opt.configfile);
230
231    #[cfg(feature = "debug")]
232    println!("Config: {:#?}", config);
233
234    // Let options override config values
235    merge_value(&mut config.database_file, opt.databasefile);
236    merge_value(&mut config.database_url, opt.databaseurl);
237    merge_value(&mut config.port, opt.port);
238    merge_value(&mut config.template, opt.template);
239    merge_value(&mut config.auto_save_interval, opt.autosaveinterval);
240
241    merge_flag(&mut config.no_contest_scan, opt.nocontestscan);
242    merge_flag(&mut config.open_browser, opt.openbrowser);
243    merge_flag(&mut config.disable_results_page, opt.disableresultspage);
244    merge_flag(&mut config.enable_password_login, opt.enablepasswordlogin);
245    merge_flag(&mut config.only_contest_scan, opt.onlycontestscan);
246    merge_flag(&mut config.reset_admin_pw, opt.resetadminpw);
247    merge_flag(&mut config.log_timing, opt.logtiming);
248
249    if let Some(template_params) = &mut config.template_params {
250        if let Some(teacherpage) = opt.teacherpage {
251            template_params.insert("teacher_page".to_string(), teacherpage.into());
252        }
253
254        let all_categories = if let Some(serde_json::Value::Object(categories)) = template_params.get("categories") {
255            Some(categories.clone())
256        } else {
257            None
258        };
259
260        if let Some(serde_json::Value::Array(tiles)) = template_params.get_mut("index_tiles") {
261            for (i, elem) in tiles.iter_mut().enumerate() {
262                if let serde_json::Value::Object(tile) = elem {
263                    if i == 0 {
264                        tile.insert("medal_index_tile_is_first".to_string(), serde_json::Value::Bool(true));
265                    }
266                    if i % 2 == 0 {
267                        tile.insert("medal_index_tile_is_even".to_string(), serde_json::Value::Bool(true));
268                    }
269                    if let Some(serde_json::Value::String(category_name)) = tile.get("category") {
270                        if let Some(Some(serde_json::Value::Object(category))) =
271                            all_categories.as_ref().map(|c| c.get(category_name))
272                        {
273                            for (key, value) in category {
274                                tile.insert(key.clone(), value.clone());
275                            }
276                        }
277                    }
278                }
279            }
280        }
281    } else if let Some(teacherpage) = opt.teacherpage {
282        let mut template_params = ::std::collections::BTreeMap::<String, serde_json::Value>::new();
283        template_params.insert("teacher_page".to_string(), teacherpage.into());
284        config.template_params = Some(template_params);
285    }
286
287    // Use default database file if none set
288    config.database_file.get_or_insert(Path::new("medal.db").to_owned());
289
290    config
291}