1
use crate::{
2
    auth::{load_jwks, Claims, Jwks},
3
    config::Config,
4
    data_format::{Rejection, Report, ReviewRequest, ReviewType, Task},
5
    error::{Error, Result},
6
    mailer::Mailer,
7
    util::load_from_json,
8
};
9
use sha2::Digest;
10
use std::{
11
    collections::BTreeMap,
12
    fs::{create_dir_all, read_dir},
13
    path::{Path, PathBuf},
14
};
15
use tokio::sync::Mutex;
16

            
17
struct DataInner {
18
    requests: BTreeMap<String, ReviewRequest>,
19
    tasks: BTreeMap<String, Task>,
20
    rejections: BTreeMap<String, Rejection>,
21
    reports: BTreeMap<String, Report>,
22
    mailer: Mailer,
23
}
24

            
25
pub struct Data {
26
    inner: Mutex<DataInner>,
27
    data_path: PathBuf,
28
    jwks: Jwks,
29
    pub(crate) config: Config,
30
}
31

            
32
impl Data {
33
9
    pub fn new(data_path: PathBuf) -> Result<Self> {
34
9
        load(data_path)
35
9
    }
36
9
    pub fn data_path(&self) -> &Path {
37
9
        &self.data_path
38
9
    }
39
    pub fn selectors(&self) -> &[String] {
40
        &self.config.selectors
41
    }
42
    pub fn is_selector(&self, claims: &Claims) -> bool {
43
        self.config.selectors.contains(&claims.organization)
44
    }
45
    pub fn review_types(&self) -> &BTreeMap<String, ReviewType> {
46
        &self.config.review_types
47
    }
48
    pub async fn get_requests(&self, claims: &Claims) -> BTreeMap<String, ReviewRequest> {
49
        let data = self.inner.lock().await;
50
        if self.is_selector(claims) {
51
            return data.requests.clone();
52
        }
53
        // For a reviewer, get all requests for which there is at least one
54
        // task.
55
        data.requests
56
            .iter()
57
            .filter(|r| data.tasks.iter().any(|t| &t.1.request == r.0))
58
            .map(|(k, v)| {
59
                let mut v = v.clone();
60
                // If the organization has no work on this request,
61
                // remove the personal information from the request.
62
                if !data
63
                    .tasks
64
                    .iter()
65
                    .any(|t| &t.1.request == k && t.1.assigned_organization == claims.organization)
66
                {
67
                    v.remove_personal_information();
68
                }
69
                (k.clone(), v)
70
            })
71
            .collect()
72
    }
73
3
    pub async fn add_request(&self, bytes: &[u8]) -> Result<ReviewRequest> {
74
1
        let mut data = self.inner.lock().await;
75
1
        let (name, request) = add_request(&mut data, bytes, None)?;
76
1
        let path = self.data_path.join("requests").join(name);
77
1
        tokio::fs::write(&path, bytes)
78
1
            .await
79
1
            .map_err(|e| Error::Io { source: e, path })?;
80
1
        let subject = "Your request to NGI Review";
81
1
        let body = format!(
82
1
            "A request was made to NGI Review with this e-mail address. Here is a structured version of that request.\n\n{}",
83
1
            serde_json::to_string_pretty(&request).map_err(|e| Error::Serde { source: e, path: None })?
84
        );
85
1
        data.mailer
86
1
            .send_mail(
87
1
                subject,
88
1
                &body,
89
1
                &request.email,
90
1
                &request.name,
91
1
                &["ngizero-review-coordinator@nlnet.nl"],
92
1
            )
93
1
            .map_err(|e| {
94
                eprintln!("{:?}", e);
95
                e
96
1
            })?;
97
1
        Ok(request)
98
1
    }
99
    pub async fn get_rejections(&self, claims: &Claims) -> BTreeMap<String, Rejection> {
100
        let data = self.inner.lock().await;
101
        if self.is_selector(claims) {
102
            return data.rejections.clone();
103
        }
104
        Default::default()
105
    }
106
    pub async fn add_rejection(&self, bytes: &[u8]) -> Result<Rejection> {
107
        let mut data = self.inner.lock().await;
108
        let (name, request, rejection) = add_rejection(&mut data, bytes, None)?;
109
        let path = self.data_path.join("rejections").join(name);
110
        tokio::fs::write(&path, bytes)
111
            .await
112
            .map_err(|e| Error::Io { source: e, path })?;
113
        let subject = "Your request to NGI Review";
114
        let body = format!(
115
            "Your request of {} was denied.\n\n{}",
116
            request.date, rejection.rejection_comment
117
        );
118
        data.mailer
119
            .send_mail(
120
                subject,
121
                &body,
122
                &request.email,
123
                &request.name,
124
                &["webmaster@nlnet.nl"],
125
            )
126
            .map_err(|e| {
127
                eprintln!("{:?}", e);
128
                e
129
            })?;
130
        Ok(rejection)
131
    }
132
    pub async fn tasks(&self) -> BTreeMap<String, Task> {
133
        self.inner.lock().await.tasks.clone()
134
    }
135
    pub async fn add_task(&self, config: &Config, bytes: &[u8]) -> Result<Task> {
136
        let mut data = self.inner.lock().await;
137
        let (name, task) = add_task(&mut data, bytes, None)?;
138
        let path = self.data_path.join("tasks").join(name);
139
        tokio::fs::write(&path, bytes)
140
            .await
141
            .map_err(|e| Error::Io { source: e, path })?;
142
        let org = if let Some(org) = config.review_types.get(&task.review_type) {
143
            org
144
        } else {
145
            return Err(Error::Msg(format!(
146
                "{} is not a known review type.",
147
                task.review_type
148
            )));
149
        };
150
        let subject = "There is a new task for NGI Review";
151
        let body = "Please visit https://dashboard.nlnet.nl/ to take a look.";
152
        // the first contact mail gets 'to', any others are 'cc'
153
        let cc: Vec<_> = org.emails.iter().skip(1).map(|s| s.as_str()).collect();
154
        data.mailer
155
            .send_mail(subject, body, &org.emails[0], &org.name, &cc)?;
156
        Ok(task)
157
    }
158
    pub async fn reports(&self) -> BTreeMap<String, Report> {
159
        self.inner.lock().await.reports.clone()
160
    }
161
    pub async fn add_report(&self, bytes: &[u8], claims: &Claims) -> Result<Report> {
162
        let mut data = self.inner.lock().await;
163
        let (name, report) = add_report_with_claims(&mut data, bytes, None, Some(claims))?;
164
        let path = self.data_path.join("reports").join(name);
165
        tokio::fs::write(&path, bytes)
166
            .await
167
            .map_err(|e| Error::Io { source: e, path })?;
168
        Ok(report)
169
    }
170
    pub async fn add_file(&self, bytes: &[u8]) -> Result<String> {
171
        let name = format!("{:x}", sha2::Sha256::digest(bytes));
172
        let path = self.data_path.join("files").join(&name);
173
        if !path.try_exists().unwrap_or(false) {
174
            tokio::fs::write(&path, bytes)
175
                .await
176
                .map_err(|e| Error::Io { source: e, path })?;
177
        }
178
        Ok(name)
179
    }
180
    pub fn jwks(&self) -> &Jwks {
181
        &self.jwks
182
    }
183
9
    pub fn config(&self) -> &Config {
184
9
        &self.config
185
9
    }
186
}
187

            
188
9
fn load(data_path: PathBuf) -> Result<Data> {
189
9
    let jwks = load_jwks(&data_path)?;
190
9
    let config_path = data_path.join("config.json");
191
9
    let config: Config = load_from_json(config_path)?;
192
90
    for (name, rt) in config.review_types.iter() {
193
90
        if rt.emails.is_empty() {
194
            return Err(Error::Msg(format!(
195
                "There are no emails defined for {}.",
196
                name
197
            )));
198
90
        }
199
    }
200
9
    let path = data_path.join("mail_config.json");
201
9
    let mail_config = load_from_json(path)?;
202
9
    let mailer = Mailer::new(mail_config)?;
203
9
    let mut data = DataInner {
204
9
        requests: BTreeMap::new(),
205
9
        rejections: BTreeMap::new(),
206
9
        tasks: BTreeMap::new(),
207
9
        reports: BTreeMap::new(),
208
9
        mailer,
209
9
    };
210
9
    load_items(&data_path, &mut data, "requests", add_request)?;
211
9
    load_items(&data_path, &mut data, "rejections", add_rejection)?;
212
9
    load_items(&data_path, &mut data, "tasks", add_task)?;
213
9
    load_items(&data_path, &mut data, "reports", add_report)?;
214
9
    let path = data_path.join("files");
215
9
    create_dir_all(&path).map_err(|e| Error::Io { source: e, path })?;
216
9
    Ok(Data {
217
9
        inner: Mutex::new(data),
218
9
        data_path,
219
9
        jwks,
220
9
        config,
221
9
    })
222
9
}
223

            
224
36
fn load_items<T>(
225
36
    path: &Path,
226
36
    data: &mut DataInner,
227
36
    dir: &str,
228
36
    add: fn(&mut DataInner, &[u8], Option<PathBuf>) -> Result<T>,
229
36
) -> Result<()> {
230
36
    let path = path.join(dir);
231
36
    create_dir_all(&path).map_err(|e| Error::Io {
232
        source: e,
233
        path: path.clone(),
234
36
    })?;
235
36
    for entry in read_dir(&path).map_err(|e| Error::Io {
236
        source: e,
237
        path: path.clone(),
238
36
    })? {
239
        let entry = entry.map_err(|e| Error::Io {
240
            source: e,
241
            path: path.clone(),
242
        })?;
243
        let path = entry.path();
244
        let bytes = std::fs::read(&path).map_err(|e| Error::Io {
245
            source: e,
246
            path: path.clone(),
247
        })?;
248
        add(data, &bytes, Some(path))?;
249
    }
250
36
    Ok(())
251
36
}
252

            
253
3
fn add_request(
254
3
    data: &mut DataInner,
255
3
    bytes: &[u8],
256
3
    path: Option<PathBuf>,
257
3
) -> Result<(String, ReviewRequest)> {
258
3
    let name = format!("{:x}", sha2::Sha256::digest(bytes));
259
3
    let request: ReviewRequest =
260
3
        serde_json::from_slice(bytes).map_err(|e| Error::Serde { source: e, path })?;
261
3
    data.requests.insert(name.clone(), request.clone());
262
3
    Ok((name, request))
263
3
}
264

            
265
fn ensure_new(data: &mut DataInner, request_id: &str, review_type: &str) -> Result<ReviewRequest> {
266
    // check the references
267
    let request = if let Some(request) = data.requests.get(request_id) {
268
        request
269
    } else {
270
        return Err(Error::Msg(format!(
271
            "request '{}' does not exist.",
272
            request_id
273
        )));
274
    };
275
    // check that there is not another task or rejection for the same request
276
    if data
277
        .tasks
278
        .iter()
279
        .any(|t| t.1.request == request_id && t.1.review_type == review_type)
280
    {
281
        return Err(Error::Msg(format!(
282
            "There is already a task for request {} and type {}.",
283
            request_id, review_type
284
        )));
285
    }
286
    if data
287
        .rejections
288
        .iter()
289
        .any(|t| t.1.request == request_id && t.1.review_type == review_type)
290
    {
291
        return Err(Error::Msg(format!(
292
            "There is already a rejection for request {} and type {}.",
293
            request_id, review_type
294
        )));
295
    }
296
    Ok(request.clone())
297
}
298

            
299
fn add_rejection(
300
    data: &mut DataInner,
301
    bytes: &[u8],
302
    path: Option<PathBuf>,
303
) -> Result<(String, ReviewRequest, Rejection)> {
304
    let name = format!("{:x}", sha2::Sha256::digest(bytes));
305
    let rejection: Rejection =
306
        serde_json::from_slice(bytes).map_err(|e| Error::Serde { source: e, path })?;
307
    let request = ensure_new(data, &rejection.request, &rejection.review_type)?;
308
    data.rejections.insert(name.clone(), rejection.clone());
309
    Ok((name, request, rejection))
310
}
311

            
312
fn add_task(data: &mut DataInner, bytes: &[u8], path: Option<PathBuf>) -> Result<(String, Task)> {
313
    let name = format!("{:x}", sha2::Sha256::digest(bytes));
314
    let task: Task = serde_json::from_slice(bytes).map_err(|e| Error::Serde { source: e, path })?;
315
    ensure_new(data, &task.request, &task.review_type)?;
316
    data.tasks.insert(name.clone(), task.clone());
317
    Ok((name, task))
318
}
319

            
320
fn add_report(
321
    data: &mut DataInner,
322
    bytes: &[u8],
323
    path: Option<PathBuf>,
324
) -> Result<(String, Report)> {
325
    add_report_with_claims(data, bytes, path, None)
326
}
327

            
328
fn add_report_with_claims(
329
    data: &mut DataInner,
330
    bytes: &[u8],
331
    path: Option<PathBuf>,
332
    claims: Option<&Claims>,
333
) -> Result<(String, Report)> {
334
    let name = format!("{:x}", sha2::Sha256::digest(bytes));
335
    let report: Report =
336
        serde_json::from_slice(bytes).map_err(|e| Error::Serde { source: e, path })?;
337
    // check the references
338
    if let Some(task) = data.tasks.get(&report.task) {
339
        if let Some(claims) = claims {
340
            if task.assigned_organization != claims.organization {
341
                return Err(Error::Msg(format!(
342
                    "task '{}' is not assigned to {}.",
343
                    report.task, claims.organization
344
                )));
345
            }
346
        }
347
    } else {
348
        println!("tasks: {:?}", data.tasks.keys());
349
        return Err(Error::Msg(format!(
350
            "Task '{}' does not exist.",
351
            report.task
352
        )));
353
    }
354
    // TODO: check that the previous report is referenced
355
    /*
356
    // check that there is no report yet for this task
357
    if data.reports.iter().any(|r| r.1.task == report.task) {
358
        return Err(Error::Msg(format!(
359
            "Task '{}' was already reported on.",
360
            report.task
361
        )));
362
    }*/
363
    data.reports.insert(name.clone(), report.clone());
364
    Ok((name, report))
365
}